@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.
- package/README.md +139 -0
- package/dist/answers.js +332 -0
- package/dist/answers.js.map +1 -0
- package/dist/cli-notionx.js +388 -0
- package/dist/cli-notionx.js.map +1 -0
- package/dist/cli-notionx.test.js +277 -0
- package/dist/cli-notionx.test.js.map +1 -0
- package/dist/diff.js +40 -0
- package/dist/diff.js.map +1 -0
- package/dist/diff.test.js +90 -0
- package/dist/diff.test.js.map +1 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-add/apply.js +39 -0
- package/dist/locale-add/apply.js.map +1 -0
- package/dist/locale-add/format.js +38 -0
- package/dist/locale-add/format.js.map +1 -0
- package/dist/locale-add/list.js +44 -0
- package/dist/locale-add/list.js.map +1 -0
- package/dist/locale-add/list.test.js +45 -0
- package/dist/locale-add/list.test.js.map +1 -0
- package/dist/locale-add/plan.js +128 -0
- package/dist/locale-add/plan.js.map +1 -0
- package/dist/locale-add/validate.js +46 -0
- package/dist/locale-add/validate.js.map +1 -0
- package/dist/metadata.js +41 -0
- package/dist/metadata.js.map +1 -0
- package/dist/notion-translation-sources/apply.js +61 -0
- package/dist/notion-translation-sources/apply.js.map +1 -0
- package/dist/notion-translation-sources/index.js +3 -0
- package/dist/notion-translation-sources/index.js.map +1 -0
- package/dist/notion-translation-sources/plan.js +33 -0
- package/dist/notion-translation-sources/plan.js.map +1 -0
- package/dist/notionx-source.js +142 -0
- package/dist/notionx-source.js.map +1 -0
- package/dist/notionx-source.test.js +144 -0
- package/dist/notionx-source.test.js.map +1 -0
- package/dist/password.js +18 -0
- package/dist/password.js.map +1 -0
- package/dist/presets.js +83 -0
- package/dist/presets.js.map +1 -0
- package/dist/presets.test.js +50 -0
- package/dist/presets.test.js.map +1 -0
- package/dist/prompt.js +218 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provision/cloudflare.js +236 -0
- package/dist/provision/cloudflare.js.map +1 -0
- package/dist/provision/dependencies.js +219 -0
- package/dist/provision/dependencies.js.map +1 -0
- package/dist/provision/index.js +681 -0
- package/dist/provision/index.js.map +1 -0
- package/dist/provision/index.test.js +54 -0
- package/dist/provision/index.test.js.map +1 -0
- package/dist/provision/inspect.js +109 -0
- package/dist/provision/inspect.js.map +1 -0
- package/dist/provision/inspect.test.js +75 -0
- package/dist/provision/inspect.test.js.map +1 -0
- package/dist/provision/notion.js +1981 -0
- package/dist/provision/notion.js.map +1 -0
- package/dist/provision/notion.test.js +542 -0
- package/dist/provision/notion.test.js.map +1 -0
- package/dist/provision/ntn-credentials.js +198 -0
- package/dist/provision/ntn-credentials.js.map +1 -0
- package/dist/provision/options.js +15 -0
- package/dist/provision/options.js.map +1 -0
- package/dist/provision/password-hash.js +78 -0
- package/dist/provision/password-hash.js.map +1 -0
- package/dist/provision/prompts.js +115 -0
- package/dist/provision/prompts.js.map +1 -0
- package/dist/provision/repair.js +48 -0
- package/dist/provision/repair.js.map +1 -0
- package/dist/provision/repair.test.js +141 -0
- package/dist/provision/repair.test.js.map +1 -0
- package/dist/provision/shell.js +84 -0
- package/dist/provision/shell.js.map +1 -0
- package/dist/provision/wire.js +78 -0
- package/dist/provision/wire.js.map +1 -0
- package/dist/registry/doctor.js +181 -0
- package/dist/registry/doctor.js.map +1 -0
- package/dist/registry/doctor.test.js +180 -0
- package/dist/registry/doctor.test.js.map +1 -0
- package/dist/registry/install.js +217 -0
- package/dist/registry/install.js.map +1 -0
- package/dist/registry/install.test.js +168 -0
- package/dist/registry/install.test.js.map +1 -0
- package/dist/registry/load-registry.js +24 -0
- package/dist/registry/load-registry.js.map +1 -0
- package/dist/registry/load-registry.test.js +59 -0
- package/dist/registry/load-registry.test.js.map +1 -0
- package/dist/registry/migration-planner.js +204 -0
- package/dist/registry/migration-planner.js.map +1 -0
- package/dist/registry/migration-planner.test.js +340 -0
- package/dist/registry/migration-planner.test.js.map +1 -0
- package/dist/registry/migrations-store.js +125 -0
- package/dist/registry/migrations-store.js.map +1 -0
- package/dist/registry/migrations-store.test.js +163 -0
- package/dist/registry/migrations-store.test.js.map +1 -0
- package/dist/registry/migrations-types.js +25 -0
- package/dist/registry/migrations-types.js.map +1 -0
- package/dist/registry/project-meta.js +84 -0
- package/dist/registry/project-meta.js.map +1 -0
- package/dist/registry/registry-items.js +354 -0
- package/dist/registry/registry-items.js.map +1 -0
- package/dist/registry/registry-items.test.js +99 -0
- package/dist/registry/registry-items.test.js.map +1 -0
- package/dist/registry/registry-store.js +232 -0
- package/dist/registry/registry-store.js.map +1 -0
- package/dist/registry/registry-store.test.js +136 -0
- package/dist/registry/registry-store.test.js.map +1 -0
- package/dist/registry/registry-types.js +18 -0
- package/dist/registry/registry-types.js.map +1 -0
- package/dist/registry/registry-types.test.js +146 -0
- package/dist/registry/registry-types.test.js.map +1 -0
- package/dist/registry/render-content-source-files.js +158 -0
- package/dist/registry/render-content-source-files.js.map +1 -0
- package/dist/registry/render-multi-source.js +296 -0
- package/dist/registry/render-multi-source.js.map +1 -0
- package/dist/registry/render-multi-source.test.js +110 -0
- package/dist/registry/render-multi-source.test.js.map +1 -0
- package/dist/registry/text-utils.js +42 -0
- package/dist/registry/text-utils.js.map +1 -0
- package/dist/registry/uninstall.js +250 -0
- package/dist/registry/uninstall.js.map +1 -0
- package/dist/registry/uninstall.test.js +264 -0
- package/dist/registry/uninstall.test.js.map +1 -0
- package/dist/registry/update.js +280 -0
- package/dist/registry/update.js.map +1 -0
- package/dist/registry/update.test.js +229 -0
- package/dist/registry/update.test.js.map +1 -0
- package/dist/render.js +549 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.js +414 -0
- package/dist/render.test.js.map +1 -0
- package/dist/templates/.dev.vars.example.tmpl +32 -0
- package/dist/templates/.gitignore.tmpl +58 -0
- package/dist/templates/README.md.tmpl +417 -0
- package/dist/templates/app/[slug]/page.tsx.tmpl +55 -0
- package/dist/templates/app/admin/account/page.tsx.tmpl +18 -0
- package/dist/templates/app/admin/content-models/page.tsx.tmpl +6 -0
- package/dist/templates/app/admin/layout.tsx.tmpl +90 -0
- package/dist/templates/app/admin/loading.tsx.tmpl +6 -0
- package/dist/templates/app/admin/page.tsx.tmpl +17 -0
- package/dist/templates/app/api/auth/google/callback/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/google/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/verify-email/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/viewer/route.ts.tmpl +3 -0
- package/dist/templates/app/api/health/route.ts.tmpl +3 -0
- package/dist/templates/app/api/{{contentSourceId}}/[slug]/route.ts.tmpl +27 -0
- package/dist/templates/app/api/{{contentSourceId}}/route.ts.tmpl +18 -0
- package/dist/templates/app/globals.css.tmpl +109 -0
- package/dist/templates/app/layout.tsx.tmpl +56 -0
- package/dist/templates/app/login/page.tsx.tmpl +154 -0
- package/dist/templates/app/page.fallback.tsx.tmpl +31 -0
- package/dist/templates/app/page.tsx.tmpl +42 -0
- package/dist/templates/app/register/page.tsx.tmpl +138 -0
- package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +113 -0
- package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +74 -0
- package/dist/templates/components/content/post-card.tsx.tmpl +80 -0
- package/dist/templates/components/notion-blocks.tsx.tmpl +668 -0
- package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +68 -0
- package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +73 -0
- package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +59 -0
- package/dist/templates/components/page-blocks/story-block.tsx.tmpl +70 -0
- package/dist/templates/components/page-blocks.fallback.tsx.tmpl +17 -0
- package/dist/templates/components/page-blocks.tsx.tmpl +32 -0
- package/dist/templates/components/search/search-dialog.tsx.tmpl +171 -0
- package/dist/templates/components/site/locale-switcher.tsx.tmpl +65 -0
- package/dist/templates/components/site/site-footer.tsx.tmpl +106 -0
- package/dist/templates/components/site/site-header.tsx.tmpl +80 -0
- package/dist/templates/components/site/site-shell.tsx.tmpl +20 -0
- package/dist/templates/components/site/theme-bootstrap.tsx.tmpl +51 -0
- package/dist/templates/components/theme-provider.tsx.tmpl +14 -0
- package/dist/templates/components/theme-toggle.tsx.tmpl +38 -0
- package/dist/templates/components/ui/accordion.tsx.tmpl +56 -0
- package/dist/templates/components/ui/alert.tsx.tmpl +59 -0
- package/dist/templates/components/ui/aspect-ratio.tsx.tmpl +8 -0
- package/dist/templates/components/ui/avatar.tsx.tmpl +44 -0
- package/dist/templates/components/ui/badge.tsx.tmpl +33 -0
- package/dist/templates/components/ui/button.tsx.tmpl +56 -0
- package/dist/templates/components/ui/card.tsx.tmpl +61 -0
- package/dist/templates/components/ui/checkbox.tsx.tmpl +28 -0
- package/dist/templates/components/ui/dialog.tsx.tmpl +104 -0
- package/dist/templates/components/ui/dropdown-menu.tsx.tmpl +183 -0
- package/dist/templates/components/ui/input.tsx.tmpl +21 -0
- package/dist/templates/components/ui/label.tsx.tmpl +25 -0
- package/dist/templates/components/ui/popover.tsx.tmpl +30 -0
- package/dist/templates/components/ui/radio-group.tsx.tmpl +44 -0
- package/dist/templates/components/ui/select.tsx.tmpl +150 -0
- package/dist/templates/components/ui/separator.tsx.tmpl +30 -0
- package/dist/templates/components/ui/sheet.tsx.tmpl +125 -0
- package/dist/templates/components/ui/skeleton.tsx.tmpl +15 -0
- package/dist/templates/components/ui/sonner.tsx.tmpl +30 -0
- package/dist/templates/components/ui/switch.tsx.tmpl +29 -0
- package/dist/templates/components/ui/table.tsx.tmpl +107 -0
- package/dist/templates/components/ui/tabs.tsx.tmpl +55 -0
- package/dist/templates/components/ui/textarea.tsx.tmpl +24 -0
- package/dist/templates/components/ui/tooltip.tsx.tmpl +30 -0
- package/dist/templates/components.json.tmpl +21 -0
- package/dist/templates/env.d.ts.tmpl +32 -0
- package/dist/templates/lib/admin/actions.ts.tmpl +43 -0
- package/dist/templates/lib/admin/context.tsx.tmpl +209 -0
- package/dist/templates/lib/admin/nav.ts.tmpl +23 -0
- package/dist/templates/lib/auth.config.fallback.ts.tmpl +10 -0
- package/dist/templates/lib/auth.config.ts.tmpl +45 -0
- package/dist/templates/lib/blocks/translations.ts.tmpl +44 -0
- package/dist/templates/lib/blog/translations.ts.tmpl +52 -0
- package/dist/templates/lib/content/models.ts.tmpl +53 -0
- package/dist/templates/lib/i18n/config.ts.tmpl +18 -0
- package/dist/templates/lib/i18n/index.ts.tmpl +1 -0
- package/dist/templates/lib/locale-contract/built-in.ts.tmpl +19 -0
- package/dist/templates/lib/locale-contract/index.ts.tmpl +3 -0
- package/dist/templates/lib/locale-contract/paths.ts.tmpl +29 -0
- package/dist/templates/lib/pages/model.ts.tmpl +16 -0
- package/dist/templates/lib/pages/source.ts.tmpl +566 -0
- package/dist/templates/lib/pages/translations.ts.tmpl +34 -0
- package/dist/templates/lib/search/config.fallback.ts.tmpl +11 -0
- package/dist/templates/lib/search/config.ts.tmpl +25 -0
- package/dist/templates/lib/site/config.ts.tmpl +120 -0
- package/dist/templates/lib/site/request-env.ts.tmpl +71 -0
- package/dist/templates/lib/site/settings.fallback.ts.tmpl +21 -0
- package/dist/templates/lib/site/settings.ts.tmpl +320 -0
- package/dist/templates/lib/site/translations.ts.tmpl +30 -0
- package/dist/templates/lib/utils.ts.tmpl +9 -0
- package/dist/templates/migrations/0001_init.sql.tmpl +57 -0
- package/dist/templates/migrations/0002_admin_seed.sql.tmpl +30 -0
- package/dist/templates/migrations/0003_search_index.sql.tmpl +29 -0
- package/dist/templates/next.config.ts.tmpl +18 -0
- package/dist/templates/package.json.tmpl +40 -0
- package/dist/templates/shims/cloudflare-workers-empty.mjs +4 -0
- package/dist/templates/shims/next-headers-empty.mjs +4 -0
- package/dist/templates/tests/smoke.test.ts.tmpl +83 -0
- package/dist/templates/tsconfig.json.tmpl +31 -0
- package/dist/templates/vite.config.ts.tmpl +53 -0
- package/dist/templates/vitest.config.ts.tmpl +13 -0
- package/dist/templates/worker/index.ts.tmpl +52 -0
- package/dist/templates/wrangler.jsonc.tmpl +44 -0
- package/dist/ui-presets.js +60 -0
- package/dist/ui-presets.js.map +1 -0
- 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,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,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,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
|
+
}
|