@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,417 @@
1
+ # {{projectName}}
2
+
3
+ A [vinext](https://github.com/digwis/nextion) project scaffolded with
4
+ [`@notionx/create-notionx-app`](https://www.npmjs.com/package/@notionx/create-notionx-app),
5
+ powered by [`@notionx/core`](https://www.npmjs.com/package/@notionx/core) on
6
+ Cloudflare Workers + D1 + R2 + Cloudflare Images.
7
+
8
+ ## Quick start
9
+
10
+ When you ran `@notionx/create-notionx-app` interactively and answered "yes" to
11
+ the provisioning prompt, this project is already wired with:
12
+
13
+ - A real Cloudflare D1 database (id written into `wrangler.jsonc` + `.dev.vars`)
14
+ - A `CONTENT_CACHE` KV namespace
15
+ - A `{{projectNameLower}}-assets` R2 bucket
16
+ - D1 migrations applied to the local Miniflare store
17
+ - Optionally: a Turnstile widget (sitekey + secret), Notion data sources
18
+ for editable site pages and seeded sample posts, Resend email verification,
19
+ Google OAuth
20
+ - Optional secrets pushed to the worker via `wrangler secret put`
21
+ (`NOTION_TOKEN`, `NOTION_DATA_SOURCE_ID`, and
22
+ `NOTION_PAGES_DATA_SOURCE_ID` when Notion was auto-created)
23
+
24
+ If provisioning was skipped or any step is ⚠️ in the scaffold summary, jump
25
+ to [Manual setup](#manual-setup) below.
26
+
27
+ ```bash
28
+ pnpm install
29
+ pnpm dev # boots vinext on http://localhost:3001
30
+ ```
31
+
32
+ ## Admin login
33
+
34
+ The scaffolder seeds the admin email you entered into D1. If you did
35
+ not pass `--admin-password`, it generated an initial password and
36
+ printed it once at the end of scaffolding.
37
+
38
+ Sign in at `/login`, then change the password from `/admin/account`.
39
+
40
+ ## Languages
41
+
42
+ `lib/site/config.ts` contains two locale settings:
43
+
44
+ - `defaultLocale` is the current fallback language for pages, metadata, and
45
+ content lookups.
46
+ - `locales` is the full set of languages the project is allowed to render.
47
+
48
+ A single-language project can keep both values to the same locale. If you later
49
+ add translations, keep the original `defaultLocale` and add new entries to
50
+ `locales`.
51
+
52
+ ## Multilingual foundation
53
+
54
+ This project ships with a built-in multilingual foundation that covers the four core models:
55
+
56
+ - `blog` — base + `blog-translations`
57
+ - `pages` — base + `page-translations`
58
+ - `blocks` — base + `block-translations`
59
+ - `site settings` — base + `site-settings-translations`
60
+
61
+ The runtime helpers live under `lib/i18n/` and `lib/locale-contract/`. The contracts are imported from `@notionx/core` and pinned in `lib/locale-contract/built-in.ts`.
62
+
63
+ Fallback rules per model:
64
+
65
+ - `blog` — missing translations are hidden from the list
66
+ - `pages` — missing translations are treated as "page does not exist in this locale"
67
+ - `blocks` — missing translations fall back to the default-locale copy
68
+ - `site settings` — missing translations fall back to the default-locale copy
69
+
70
+ The header renders a `LocaleSwitcher` automatically when more than one locale is configured. Adding a new locale to an existing project is a separate `notionx locale add <locale>` workflow.
71
+
72
+ ## Custom content models
73
+
74
+ To add a new content model to the locale foundation (for example a `products` source), declare a contract in `lib/locale-contract/<model>.ts` using the `defineLocaleContract` helper. A full worked example lives in [the locale-contract extension example](https://github.com/digwis/nextion/blob/main/docs/locale-contract-extension-example.md). The same shape works for jobs, events, recipes, and any other model.
75
+
76
+ ## Inspecting current locales
77
+
78
+ ```bash
79
+ npx notionx locale list
80
+ ```
81
+
82
+ Prints the current supported locales plus the status of each built-in translation data source (`configured` or `missing`). Use the output to confirm a `notionx locale add` ran cleanly.
83
+
84
+ ## Adding a locale to an existing project
85
+
86
+ ```bash
87
+ # 1. Dry run — see exactly what would change, no Notion calls.
88
+ npx notionx locale add zh-CN
89
+
90
+ # 2. Apply the local changes (metadata + i18n config + site config).
91
+ npx notionx locale add zh-CN --apply
92
+
93
+ # 3. (Optional) Provision the four translation data sources in
94
+ # Notion and sync the new ids as worker secrets.
95
+ npx notionx locale add zh-CN --with-notion --apply \
96
+ --copy-from en
97
+ ```
98
+
99
+ The command refuses to remove or overwrite existing locales. It only ever adds, and the local pass never contacts Notion. Re-running with the same locale is a no-op (the validator returns "already in supportedLocales").
100
+
101
+ After adding a locale, edit the translation data sources in Notion to fill in the locale-specific copy. The `LocaleSwitcher` in the header picks up the new locale automatically on the next deploy.
102
+
103
+ ## Notion-backed pages
104
+
105
+ The starter has two Notion concepts:
106
+
107
+ - `Pages` controls site routes such as `/`, `/about`, `/privacy`, and the
108
+ editable shell around list pages.
109
+ - `{{contentSourceTitle}}` controls the actual list/detail content items.
110
+
111
+ In `Pages`, `Show Header` / `Show Footer` control whether the current page
112
+ renders the global header and footer. `Show in Nav` / `Show in Footer` control
113
+ whether that page appears as a link in the header or footer. `Nav Order` and
114
+ `Footer Order` sort those links. For content list pages, set `Layout` to
115
+ `content-list` and `Content Source` to `{{contentSourceId}}`.
116
+
117
+ Future translations should use a separate translations database related back
118
+ to `Pages`, mirroring the pattern used for richer content models: the base row
119
+ keeps stable structure (`Key`, layout, nav/footer switches), while translation
120
+ rows override locale-specific title, slug, nav label, SEO copy, and body blocks.
121
+
122
+ ## Manual setup
123
+
124
+ If you skipped provisioning or need to fill in something later:
125
+
126
+ ```bash
127
+ # 1. Cloudflare auth
128
+ pnpm dlx wrangler@latest login
129
+
130
+ # 2. D1 database
131
+ pnpm exec wrangler d1 create {{projectNameLower}}-db
132
+ # paste the printed database_id into wrangler.jsonc → d1_databases[0].database_id
133
+ # and into .dev.vars → D1_DATABASE_ID
134
+ pnpm exec wrangler d1 migrations apply {{projectNameLower}}-db --local
135
+
136
+ # 3. KV namespace
137
+ pnpm exec wrangler kv namespace create CONTENT_CACHE
138
+ # paste the printed id into wrangler.jsonc → kv_namespaces[0].id
139
+ # and into .dev.vars → KV_NAMESPACE_ID
140
+
141
+ # 4. R2 bucket
142
+ pnpm exec wrangler r2 bucket create {{projectNameLower}}-assets
143
+
144
+ # 5. Turnstile (optional)
145
+ # wrangler has no turnstile subcommand and `wrangler login`'s scope
146
+ # doesn't include Account.Turnstile:Edit, so you need a custom API
147
+ # token: https://dash.cloudflare.com/profile/api-tokens
148
+ # Required permission: Account.Turnstile:Edit
149
+ # Then create a widget at: https://dash.cloudflare.com/?to=/:account/turnstile
150
+ # Domains: localhost, 127.0.0.1, and your production hostname
151
+ # Put sitekey into .dev.vars → TURNSTILE_SITE_KEY
152
+ # Put secret into .dev.vars → TURNSTILE_SECRET_KEY
153
+ # And: echo $SECRET | pnpm exec wrangler secret put TURNSTILE_SECRET_KEY
154
+
155
+ # 6. Notion (optional)
156
+ # Create an integration at https://www.notion.so/my-integrations
157
+ # Share a target page with the integration
158
+ # Put the integration token into .dev.vars → NOTION_TOKEN
159
+ # For the default blog scaffold, create a database under that page
160
+ # with: Name, Slug, Description, Published, Date, Tags, Cover
161
+ # Article body content lives in the Notion page content blocks.
162
+ # Put its data source id into .dev.vars → NOTION_DATA_SOURCE_ID
163
+ # Create a Pages database under the same page with:
164
+ # Name, Key, Slug, Status, Layout, Description, SEO Title,
165
+ # SEO Description, Show Header, Show Footer, Show in Nav, Nav Label,
166
+ # Nav Order, Show in Footer, Footer Label, Footer Group, Footer Order,
167
+ # Content Source, Cover
168
+ # Put its data source id into .dev.vars → NOTION_PAGES_DATA_SOURCE_ID
169
+ # For production, also set all three values on the Worker:
170
+ # printf %s "$NOTION_TOKEN" | pnpm exec wrangler secret put NOTION_TOKEN
171
+ # printf %s "$NOTION_DATA_SOURCE_ID" | pnpm exec wrangler secret put NOTION_DATA_SOURCE_ID
172
+ # printf %s "$NOTION_PAGES_DATA_SOURCE_ID" | pnpm exec wrangler secret put NOTION_PAGES_DATA_SOURCE_ID
173
+ # (If the scaffolder already auto-created the database via `ntn`,
174
+ # see "Switching the Notion integration" below to use your own
175
+ # integration for ongoing API access.)
176
+ # The scaffolder also creates a SECOND Notion data source for
177
+ # site-level settings (name, tagline, description, default locale,
178
+ # social image) — its id lands in
179
+ # .dev.vars → NOTION_SITE_SETTINGS_DATA_SOURCE_ID. Edit the single
180
+ # row in that database to update your home page / SEO metadata
181
+ # without redeploying.
182
+
183
+ # 7. Resend (optional)
184
+ # Get an API key at https://resend.com/api-keys
185
+ # Put into .dev.vars → RESEND_API_KEY and RESEND_FROM
186
+
187
+ # 8. Google OAuth (optional)
188
+ # Create an OAuth client at https://console.cloud.google.com/apis/credentials
189
+ # Authorized redirect URI: $SITE_URL/api/auth/google/callback
190
+ # Put client id + secret into .dev.vars → GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
191
+ ```
192
+
193
+ ## What's included
194
+
195
+ - `app/page.tsx` — homepage rendered from the Notion-backed Pages model
196
+ - `app/[slug]/page.tsx` — generic Notion-backed pages such as `/about` and `/privacy`
197
+ - `app/api/health/route.ts` — `GET /api/health` from notionx
198
+ - `app/api/auth/*` — Google OAuth, email verify, viewer endpoints from notionx
199
+ - `app/login/page.tsx` — email/password login wired to `authConfig`
200
+ - `lib/auth.config.ts` — `AuthConfig` consumed by `createAuth`
201
+ - `lib/admin/nav.ts` — sidebar items for `/admin/*`
202
+ - `lib/i18n/config.ts` — project locale config + `isAppLocale` guard
203
+ - `lib/locale-contract/*` — built-in locale contracts, path helpers, and the LocaleSwitcher contract surface
204
+ - `lib/blog/translations.ts` — blog translation lookup helpers
205
+ - `lib/pages/translations.ts` — pages translation lookup helpers
206
+ - `lib/blocks/translations.ts` — blocks translation lookup helpers
207
+ - `lib/site/translations.ts` — site-settings translation lookup helpers
208
+ - `lib/site/config.ts` — static fallback values for site-level config
209
+ - `lib/site/settings.ts` — Notion-backed loader for the same config (KV-cached, 5 min TTL)
210
+ - `lib/pages/*` — Notion-backed Pages model, mapper, navigation, and footer helpers
211
+ - `lib/content/models.ts` — Notion-backed content sources: `{{contentSourceId}}` + `site-settings` (singleton)
212
+ - `components/site/locale-switcher.tsx` — default `LocaleSwitcher` UI
213
+ - `worker/index.ts` — `createNotionxWorker` + vinext fallthrough
214
+ - `migrations/0001_init.sql` — notionx auth schema (users, app_settings, auth_rate_limits)
215
+ - `wrangler.jsonc` — D1 + KV + R2 + Images + Assets bindings
216
+
217
+ ## Tests
218
+
219
+ ```bash
220
+ pnpm test
221
+ ```
222
+
223
+ The single sanity test under `tests/smoke.test.ts` confirms that
224
+ `defineContentSource` registered the `{{contentSourceId}}` source with the
225
+ notionx registry and that re-registration is idempotent on `id`.
226
+
227
+ ## Keeping The Project Updated
228
+
229
+ ```bash
230
+ npx notionx update
231
+ ```
232
+
233
+ The update command upgrades `@notionx/core`, syncs scaffold-managed files, and
234
+ repairs safe Notion / Cloudflare drift automatically. If an update would
235
+ overwrite customized code or populated Notion content, it pauses once and asks
236
+ for confirmation before applying those conflict updates.
237
+
238
+ ## Deploy
239
+
240
+ ```bash
241
+ pnpm exec wrangler d1 migrations apply {{projectNameLower}}-db --remote
242
+ pnpm exec wrangler kv namespace create CONTENT_CACHE
243
+ # Paste the printed id into `wrangler.jsonc` under `kv_namespaces[*].id`.
244
+ pnpm exec wrangler r2 bucket create {{projectNameLower}}-assets
245
+ pnpm exec vinext deploy
246
+ ```
247
+
248
+ ## Wiring up the @notionx/core package
249
+
250
+ The dependency is `"@notionx/core": "{{notionxSource}}"` — by default
251
+ `^0.1.2` (the version published to npm). This works for any host
252
+ machine with no extra setup.
253
+
254
+ To consume a local build of the package instead, switch the specifier
255
+ to a `link:` / `file:` / `workspace:*` reference and re-run
256
+ `pnpm install`. For monorepo development, drop a `pnpm-workspace.yaml`
257
+ next to this project:
258
+
259
+ ```yaml
260
+ packages:
261
+ - "."
262
+ - "../vinext-monorepo/packages/notionx"
263
+ ```
264
+
265
+ (Adding `.` is necessary so pnpm still treats this project as a
266
+ workspace package — otherwise the existing workspace assumption that
267
+ `"@notionx/core": "workspace:*"` is being resolved from the workspace
268
+ breaks.)
269
+
270
+ ## Customize the look
271
+
272
+ Every component reads its colors, radius, and shadow from CSS
273
+ variables in `app/globals.css`. The default palette is neutral
274
+ black/white. **Change those variables to re-skin the entire UI — no
275
+ component code needs to change.**
276
+
277
+ ```css
278
+ /* app/globals.css — :root { ... } */
279
+ --primary: 222 47% 11%; /* ← change this to re-color buttons, badges, links */
280
+ --radius: 0.5rem; /* ← bump to 1rem for chunky/pillowy UI */
281
+ --background: 0 0% 100%; /* ← page background */
282
+ --foreground: 0 0% 3.9%; /* ← main text */
283
+ --font-sans: "Inter", sans-serif; /* swap in your own typeface */
284
+ ```
285
+
286
+ Common recipes:
287
+
288
+ | Look | `--primary` | `--radius` | Notes |
289
+ |---|---|---|---|
290
+ | Linear-style (sharp, blue-gray) | `220 13% 18%` | `0.375rem` | tight spacing, no shadows |
291
+ | Stripe-style (purple) | `240 75% 60%` | `0.5rem` | generous padding |
292
+ | Vercel-style (monochrome) | `0 0% 9%` | `0.5rem` | what you have now |
293
+ | Claude-style (warm orange) | `16 53% 58%` | `0.375rem` | pair with Lora font |
294
+ | Playful / kid-friendly | `340 80% 55%` | `1.5rem` | add a display font |
295
+
296
+ For dark mode, the same tokens have a `.dark { ... }` override
297
+ block right below `:root` — keep them in sync.
298
+
299
+ ## UI components (shadcn/ui)
300
+
301
+ This project was generated with the `site` UI preset.
302
+
303
+ The starter ships with these shadcn/ui-compatible components in
304
+ `components/ui/`, ready to import anywhere in the project:
305
+
306
+ {{uiComponentList}}
307
+
308
+ Every preset includes these primitives:
309
+
310
+ - `Button` — variants: default, secondary, outline, ghost, link, destructive
311
+ - `Card` (+ `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`)
312
+ - `Badge` — variants: default, secondary, destructive, outline
313
+ - `Input`, `Label`, `Separator`, `Skeleton`
314
+ - `ThemeToggle` + `ThemeProvider` (light / dark / system)
315
+
316
+ `site` adds the components used by richer Notion page rendering:
317
+ `Accordion`, `Alert`, `Table`, `AspectRatio`, `Tabs`, `Tooltip`,
318
+ `DropdownMenu`, `Sheet`, and `Dialog`.
319
+
320
+ `app` adds form and dashboard primitives such as `Select`, `Textarea`,
321
+ `Checkbox`, `Switch`, `RadioGroup`, `Avatar`, `Popover`, and `Toaster`.
322
+
323
+ All components are themeable via the design tokens defined in
324
+ `app/globals.css` (`--background`, `--foreground`, `--primary`,
325
+ `--radius`, …). To re-skin the entire UI, edit those CSS variables —
326
+ no component code needs to change.
327
+
328
+ ### Add more components
329
+
330
+ The standard `shadcn` CLI works out of the box. `components.json` is
331
+ already configured for this project.
332
+
333
+ ```bash
334
+ # Add any shadcn/ui component
335
+ pnpm dlx shadcn@latest add dialog dropdown-menu select textarea \
336
+ sonner tooltip tabs
337
+
338
+ # Or pull a block from a registry
339
+ pnpm dlx shadcn@latest add https://ui.shadcn.com/r/login-01.json
340
+
341
+ # Add from any third-party registry (e.g. 21st.dev)
342
+ pnpm dlx shadcn@latest add https://21st.dev/r/sonner.json
343
+ ```
344
+
345
+ New files land in `components/ui/` and pick up the existing token
346
+ variables automatically. After adding, import and use as usual.
347
+
348
+ ## Notion page builder blocks
349
+
350
+ `components/notion-blocks.tsx` treats Notion as the editor and this app
351
+ as the visual renderer. Native Notion blocks drive structure, while
352
+ Tailwind CSS, React, and the local UI primitives control the public site.
353
+
354
+ Supported page-builder blocks include paragraphs, headings, grouped lists,
355
+ todos, callouts, toggles, tables, columns, images, video, audio, files,
356
+ PDFs, embeds, bookmarks, child pages, synced blocks, and safe public-link
357
+ buttons.
358
+
359
+ For high-design sections that Notion cannot express cleanly, add a code
360
+ block with language `vinext` and JSON config:
361
+
362
+ ```vinext
363
+ {
364
+ "component": "hero",
365
+ "eyebrow": "Notion-powered publishing",
366
+ "title": "Build polished pages from Notion blocks",
367
+ "description": "Authors edit in Notion. The site renders with React, Tailwind, and shadcn-style components.",
368
+ "primaryAction": {
369
+ "label": "Start building",
370
+ "href": "/register"
371
+ }
372
+ }
373
+ ```
374
+
375
+ Initial custom components: `hero`, `cta`, `feature-grid`, `pricing`,
376
+ `faq`, `content-list`, `testimonial-grid`, and `contact-form`.
377
+
378
+ ## Site settings
379
+
380
+ The `lib/site/settings.ts` module reads site-level config (name, tagline, description, default locale, social image) from a dedicated Notion data source (`NOTION_SITE_SETTINGS_DATA_SOURCE_ID`). The scaffolder creates this data source automatically alongside the main content source and seeds a single row populated with the project name.
381
+
382
+ To edit site copy:
383
+
384
+ 1. Open the site-settings database in Notion (its title is `<projectName> Site Settings`).
385
+ 2. Edit the `Site Name`, `Tagline`, `Description`, `Default Locale`, or `Social Image` columns of the single row.
386
+ 3. Changes show up within 5 minutes (KV cache TTL) — or hit the admin revalidate endpoint for an instant refresh.
387
+
388
+ If Notion is unreachable, the loader returns the hard-coded values from `lib/site/config.ts`, so the site stays up even when the CMS is down. To opt out of the Notion-backed loader entirely, re-run the scaffolder with `--no-site-settings` and revert `lib/site/settings.ts` to a one-liner that returns `siteConfig`.
389
+
390
+ ## Switching the Notion integration
391
+
392
+ If the scaffolder auto-created the content database using the `ntn`
393
+ CLI's saved credentials (you'll see "token from macOS Keychain …"
394
+ or similar in the provisioning summary), the database is **owned by
395
+ "Notion CLI"** in your Notion workspace. That is a Notion UI label
396
+ only — it does not change the data, schema, or your access rights.
397
+
398
+ If you want your own integration (`secret_…` from
399
+ <https://www.notion.so/profile/integrations>) to manage the database
400
+ going forward, do this once in the Notion UI:
401
+
402
+ 1. Open the database in Notion.
403
+ 2. Click **⋯** (top right) → **Connections** → add your own
404
+ integration. From now on your integration can read and write
405
+ this database.
406
+ 3. *(Optional)* Click **⋯** → **Transfer ownership** to make
407
+ your integration the new owner. The "Created by Notion CLI"
408
+ label disappears.
409
+
410
+ After step 2, replace the `NOTION_TOKEN` line in `.dev.vars` and
411
+ the Worker secret with your own `secret_…` token:
412
+
413
+ ```bash
414
+ printf %s "$NOTION_TOKEN" | pnpm exec wrangler secret put NOTION_TOKEN
415
+ ```
416
+
417
+ The data source id does not need to change.
@@ -0,0 +1,55 @@
1
+ import type { Metadata } from "next";
2
+ import { notFound } from "next/navigation";
3
+ import { NotionBlocks } from "@/components/notion-blocks";
4
+ import { PageBlocks } from "@/components/page-blocks";
5
+ import { SiteShell } from "@/components/site/site-shell";
6
+ import { getSitePageBySlug } from "@/lib/pages/source";
7
+
8
+ export const revalidate = 300;
9
+
10
+ type Params = { slug: string };
11
+
12
+ export async function generateMetadata({
13
+ params,
14
+ }: {
15
+ params: Promise<Params>;
16
+ }): Promise<Metadata> {
17
+ const { slug } = await params;
18
+ const page = await getSitePageBySlug(slug);
19
+ if (!page) return { title: "Not found" };
20
+ return {
21
+ title: page.seoTitle || page.title,
22
+ description: page.seoDescription || page.description,
23
+ openGraph: page.coverImage
24
+ ? { images: [{ url: page.coverImage }] }
25
+ : undefined,
26
+ };
27
+ }
28
+
29
+ export default async function SitePage({
30
+ params,
31
+ }: {
32
+ params: Promise<Params>;
33
+ }) {
34
+ const { slug } = await params;
35
+ const page = await getSitePageBySlug(slug);
36
+ if (!page || page.layout === "content-list") notFound();
37
+
38
+ return (
39
+ <SiteShell showHeader={page.showHeader} showFooter={page.showFooter}>
40
+ <main className="container mx-auto max-w-3xl px-4 py-16">
41
+ <header className="mb-10 space-y-4">
42
+ <h1 className="text-4xl font-bold tracking-tight">{page.title}</h1>
43
+ {page.description ? (
44
+ <p className="text-lg text-muted-foreground">{page.description}</p>
45
+ ) : null}
46
+ </header>
47
+ {page.structuredBlocks.length ? (
48
+ <PageBlocks blocks={page.structuredBlocks} />
49
+ ) : (
50
+ <NotionBlocks blocks={page.blocks} />
51
+ )}
52
+ </main>
53
+ </SiteShell>
54
+ );
55
+ }
@@ -0,0 +1,18 @@
1
+ import { AccountPage } from "@notionx/core/admin/pages";
2
+ import { buildAdminPageContext } from "@/lib/admin/context";
3
+
4
+ type Props = {
5
+ searchParams: Promise<{
6
+ saved?: string;
7
+ error?: string;
8
+ }>;
9
+ };
10
+
11
+ export default async function AdminAccountPage({ searchParams }: Props) {
12
+ return (
13
+ <AccountPage
14
+ context={buildAdminPageContext()}
15
+ searchParams={await searchParams}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,6 @@
1
+ import { ContentModelsPage } from "@notionx/core/admin/pages";
2
+ import { buildAdminPageContext } from "@/lib/admin/context";
3
+
4
+ export default function AdminContentModelsPage() {
5
+ return <ContentModelsPage context={buildAdminPageContext()} />;
6
+ }
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from "react";
2
+ import Link from "next/link";
3
+ import { redirect } from "next/navigation";
4
+ import { AdminShell } from "@notionx/core/admin";
5
+ import {
6
+ clearSessionCookie,
7
+ clearUserSessionCookie,
8
+ getAuthViewer,
9
+ } from "@notionx/core/auth";
10
+ import { LogOut, UserCircle } from "lucide-react";
11
+ import { adminNav } from "@/lib/admin/nav";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Separator } from "@/components/ui/separator";
14
+ import { ThemeToggle } from "@/components/theme-toggle";
15
+
16
+ async function logoutAction(): Promise<void> {
17
+ "use server";
18
+ await clearSessionCookie();
19
+ await clearUserSessionCookie();
20
+ redirect("/login");
21
+ }
22
+
23
+ export default async function AdminLayout({
24
+ children,
25
+ }: {
26
+ children: ReactNode;
27
+ }) {
28
+ const authViewer = await getAuthViewer();
29
+ if (!authViewer) redirect("/login");
30
+
31
+ const viewer = {
32
+ email: authViewer.email,
33
+ name: authViewer.user?.name ?? null,
34
+ picture: authViewer.user?.picture ?? null,
35
+ isAdmin: authViewer.isAdmin,
36
+ role: authViewer.role,
37
+ };
38
+
39
+ return (
40
+ <AdminShell
41
+ nav={adminNav}
42
+ viewer={viewer}
43
+ pathname="/admin"
44
+ brandLabel="{{projectName}} Admin"
45
+ brandHref="/admin"
46
+ viewerRoles={authViewer.isAdmin ? ["admin", "user"] : ["user"]}
47
+ headerLinks={
48
+ <>
49
+ <Separator orientation="vertical" className="h-4" />
50
+ <Link
51
+ href="{{contentSourceListPath}}"
52
+ className="text-sm text-muted-foreground hover:text-foreground"
53
+ >
54
+ 查看{{contentSourceNavLabel}}
55
+ </Link>
56
+ {authViewer.isAdmin ? (
57
+ <>
58
+ <Separator orientation="vertical" className="h-4" />
59
+ <Link
60
+ href="/admin/content-models"
61
+ className="text-sm font-medium hover:underline"
62
+ >
63
+ 内容模型
64
+ </Link>
65
+ </>
66
+ ) : null}
67
+ </>
68
+ }
69
+ headerActions={
70
+ <>
71
+ <Button asChild variant="ghost" size="sm">
72
+ <Link href="/admin/account">
73
+ <UserCircle className="mr-1 h-3 w-3" />
74
+ 账户
75
+ </Link>
76
+ </Button>
77
+ <ThemeToggle />
78
+ <form action={logoutAction}>
79
+ <Button type="submit" variant="outline" size="sm">
80
+ <LogOut className="mr-1 h-3 w-3" />
81
+ 登出
82
+ </Button>
83
+ </form>
84
+ </>
85
+ }
86
+ >
87
+ {children}
88
+ </AdminShell>
89
+ );
90
+ }
@@ -0,0 +1,6 @@
1
+ import { LoadingPage } from "@notionx/core/admin/pages";
2
+ import { buildAdminPageContext } from "@/lib/admin/context";
3
+
4
+ export default function AdminLoading() {
5
+ return <LoadingPage context={buildAdminPageContext()} />;
6
+ }
@@ -0,0 +1,17 @@
1
+ import { DashboardPage } from "@notionx/core/admin/pages";
2
+ import { buildAdminPageContext } from "@/lib/admin/context";
3
+
4
+ type Props = {
5
+ searchParams: Promise<{
6
+ error?: string;
7
+ }>;
8
+ };
9
+
10
+ export default async function AdminDashboardPage({ searchParams }: Props) {
11
+ return (
12
+ <DashboardPage
13
+ context={buildAdminPageContext()}
14
+ searchParams={await searchParams}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,3 @@
1
+ // Re-export notionx's `/api/auth/google/callback` route handler.
2
+ // See `@notionx/core/auth/routes/google-callback` for the implementation.
3
+ export { GET } from "@notionx/core/auth/routes/google-callback";
@@ -0,0 +1,3 @@
1
+ // Re-export notionx's `/api/auth/google` route handler.
2
+ // See `@notionx/core/auth/routes/google` for the implementation.
3
+ export { GET } from "@notionx/core/auth/routes/google";
@@ -0,0 +1,3 @@
1
+ // Re-export notionx's `/api/auth/verify-email` route handler.
2
+ // See `@notionx/core/auth/routes/verify-email` for the implementation.
3
+ export { GET } from "@notionx/core/auth/routes/verify-email";
@@ -0,0 +1,3 @@
1
+ // Re-export notionx's `/api/auth/viewer` route handler.
2
+ // See `@notionx/core/auth/routes/viewer` for the implementation.
3
+ export { GET } from "@notionx/core/auth/routes/viewer";
@@ -0,0 +1,3 @@
1
+ // Delegate to the package's health route handler.
2
+ // See `@notionx/core/worker/routes/health` for the implementation.
3
+ export { GET } from "@notionx/core/worker/routes/health";
@@ -0,0 +1,27 @@
1
+ // Public API: get a single {{contentSourceTitle}} item by slug.
2
+ // Returns 404 if the slug doesn't match a published item.
3
+
4
+ import { NextResponse } from "next/server";
5
+ import { getGenericNotionContentBySlug } from "@notionx/core/notion";
6
+ import { {{contentSourceVarName}} } from "@/lib/content/models";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ type Params = { slug: string };
11
+
12
+ export async function GET(
13
+ _request: Request,
14
+ { params }: { params: Promise<Params> },
15
+ ) {
16
+ const { slug } = await params;
17
+ try {
18
+ const item = await getGenericNotionContentBySlug({{contentSourceVarName}}, slug);
19
+ if (!item) {
20
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
21
+ }
22
+ return NextResponse.json({ item });
23
+ } catch (err) {
24
+ const message = err instanceof Error ? err.message : "Unknown error";
25
+ return NextResponse.json({ error: message }, { status: 500 });
26
+ }
27
+ }