create-nextly-app 0.0.2-alpha.66 → 0.0.2-alpha.68
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 +29 -8
- package/dist/{chunk-I2U7KBH6.mjs → chunk-XCYSH46I.mjs} +36805 -10917
- package/dist/chunk-XCYSH46I.mjs.map +1 -0
- package/dist/cli.cjs +27693 -1892
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +17 -106
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +36591 -10708
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +1 -1
- package/package.json +18 -7
- package/skills/nextly-authentication/SKILL.md +177 -0
- package/skills/nextly-collections/SKILL.md +215 -0
- package/skills/nextly-deploy/SKILL.md +246 -0
- package/skills/nextly-install/SKILL.md +122 -0
- package/templates/base/.env.example +3 -5
- package/templates/base/AGENTS.md.template +31 -0
- package/templates/base/src/types/generated/nextly-types.ts +6 -4
- package/templates/blank/src/app/(frontend)/components/ThemeToggle.tsx +55 -24
- package/templates/blank/src/instrumentation.ts +27 -0
- package/templates/plugin/AGENTS.md.template +6 -4
- package/templates/plugin/README.md +4 -0
- package/templates/plugin/dev/.env.example +5 -2
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +14 -2
- package/templates/plugin/dev/src/instrumentation.ts +20 -0
- package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
- package/templates/plugin/gitignore +5 -2
- package/templates/plugin/src/plugin.test.ts +9 -2
- package/dist/chunk-I2U7KBH6.mjs.map +0 -1
- package/templates/plugin/dev/instrumentation.ts +0 -10
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextly-deploy
|
|
3
|
+
description: >-
|
|
4
|
+
Use when deploying a Nextly project to production, on Vercel or a Node.js
|
|
5
|
+
server: choosing the production database, creating and committing
|
|
6
|
+
migrations, running them on deploy, the environment variables Nextly
|
|
7
|
+
requires, media storage that survives restarts, and checking the live site.
|
|
8
|
+
Also use when a deployed Nextly app has missing tables, refuses to start,
|
|
9
|
+
or loses uploaded files.
|
|
10
|
+
metadata:
|
|
11
|
+
nextly-version: 0.0.2-alpha.67
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Deploy a Nextly project
|
|
15
|
+
|
|
16
|
+
A Nextly app is a Next.js app, so it deploys wherever Next.js does. What
|
|
17
|
+
Nextly adds: a production database, migrations applied before the new code
|
|
18
|
+
serves traffic, a few environment variables, and media storage that is not
|
|
19
|
+
the local disk on serverless hosts.
|
|
20
|
+
|
|
21
|
+
Checked against `nextly` 0.0.2-alpha.67 with Next.js 16.4 and PostgreSQL 17:
|
|
22
|
+
the steps below were run end to end, from an empty database to a published
|
|
23
|
+
post in a production build. Run `npx nextly --version`; if it prints a newer
|
|
24
|
+
version, prefer <https://nextlyhq.com/docs/guides/deployment> where it
|
|
25
|
+
disagrees with this skill.
|
|
26
|
+
|
|
27
|
+
## 1. Pick the production database, and develop on the same kind
|
|
28
|
+
|
|
29
|
+
Use PostgreSQL (recommended) or MySQL in production; on Vercel, a managed one
|
|
30
|
+
such as Neon, Supabase or Vercel Postgres. SQLite does not survive on
|
|
31
|
+
serverless hosts.
|
|
32
|
+
|
|
33
|
+
Develop against the same kind of database, and switch **before** creating the
|
|
34
|
+
first migration. `migrate:create` writes SQL in the dialect of the database
|
|
35
|
+
configured when you run it, so migrations made on SQLite will not run on
|
|
36
|
+
PostgreSQL. If SQLite migrations already exist and nothing has been deployed,
|
|
37
|
+
delete `src/db/migrations/` (the `.sql` files and `meta/`) after switching and
|
|
38
|
+
run `migrate:create` again. A live SQLite installation needs its data moved,
|
|
39
|
+
which this skill does not cover.
|
|
40
|
+
|
|
41
|
+
To switch a project from SQLite to PostgreSQL:
|
|
42
|
+
|
|
43
|
+
Nextly's packages are released together, so install each `@nextlyhq/*`
|
|
44
|
+
package at exactly the version of `nextly` the project runs, which `npx nextly
|
|
45
|
+
--version` prints:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install "@nextlyhq/adapter-postgres@$(npx nextly --version)" pg # pnpm add / yarn add / bun add
|
|
49
|
+
npm ls nextly @nextlyhq/adapter-postgres # or pnpm ls / yarn why; both at the same version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use the project's own package manager throughout, so its lockfile stays the
|
|
53
|
+
one that is deployed.
|
|
54
|
+
|
|
55
|
+
Then, in `.env`, set `DB_DIALECT=postgresql` and `DATABASE_URL` to a local
|
|
56
|
+
PostgreSQL database, and add `"@nextlyhq/adapter-postgres"` and `"pg"` to
|
|
57
|
+
`serverExternalPackages` in `next.config.ts`. Restart `next dev`.
|
|
58
|
+
|
|
59
|
+
## 2. Create the migrations and commit them
|
|
60
|
+
|
|
61
|
+
In development, schema changes apply directly. Production only applies
|
|
62
|
+
migration files. Whenever collections change, before deploying:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx nextly migrate:create <what_changed>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It writes `src/db/migrations/<timestamp>_<name>.sql` and a snapshot in
|
|
69
|
+
`src/db/migrations/meta/`. Commit both. Migrations only move forward: to undo
|
|
70
|
+
one, add a new one.
|
|
71
|
+
|
|
72
|
+
## 3. Run migrations on every deploy
|
|
73
|
+
|
|
74
|
+
In `package.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "nextly migrate && next build"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`nextly migrate` first creates or updates Nextly's own tables (users, roles,
|
|
85
|
+
media and the rest), then applies your pending migrations, against the
|
|
86
|
+
database in `DATABASE_URL`. On an empty database this is all that is needed
|
|
87
|
+
before the first start.
|
|
88
|
+
|
|
89
|
+
With several developers or preview deploys pointing at one database, run
|
|
90
|
+
`npx nextly migrate` as its own CI step before the deploy instead, and keep
|
|
91
|
+
`build` as `next build`.
|
|
92
|
+
|
|
93
|
+
## 4. Environment variables
|
|
94
|
+
|
|
95
|
+
| Variable | Value |
|
|
96
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `DB_DIALECT` | `postgresql` or `mysql` |
|
|
98
|
+
| `DATABASE_URL` | the production database |
|
|
99
|
+
| `NEXTLY_SECRET` | at least 32 characters, new for production: `openssl rand -base64 32` |
|
|
100
|
+
| `NEXT_PUBLIC_APP_URL` | the public URL, such as `https://example.com` |
|
|
101
|
+
| `NEXTLY_ALLOWED_ORIGINS` | only if browsers reach the app on an origin it does not see itself, such as behind a proxy; comma-separated |
|
|
102
|
+
|
|
103
|
+
`NEXT_PUBLIC_APP_URL` is built into the client bundle, so set it before the
|
|
104
|
+
build. Never commit `.env` files.
|
|
105
|
+
|
|
106
|
+
## 5. Media storage
|
|
107
|
+
|
|
108
|
+
Uploads go to `./public/uploads` by default. That is lost on every deploy or
|
|
109
|
+
cold start on Vercel and other serverless hosts. Use a storage adapter,
|
|
110
|
+
installed at the same version as `nextly` as above:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// nextly.config.ts, on Vercel: npm install @nextlyhq/storage-vercel-blob@<nextly version>
|
|
114
|
+
import { defineConfig } from "nextly/config";
|
|
115
|
+
import { vercelBlobStorage } from "@nextlyhq/storage-vercel-blob";
|
|
116
|
+
|
|
117
|
+
export default defineConfig({
|
|
118
|
+
storage: [
|
|
119
|
+
vercelBlobStorage({
|
|
120
|
+
token: process.env.BLOB_READ_WRITE_TOKEN!,
|
|
121
|
+
collections: { media: true },
|
|
122
|
+
}),
|
|
123
|
+
],
|
|
124
|
+
// collections, typescript, ...
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Vercel sets `BLOB_READ_WRITE_TOKEN` when Blob storage is added to the project.
|
|
129
|
+
For AWS S3, use `@nextlyhq/storage-s3` with
|
|
130
|
+
`s3Storage({ bucket, region, accessKeyId, secretAccessKey, collections: { media: true } })`.
|
|
131
|
+
Other S3-compatible providers also need their `endpoint`, and a `publicUrl`
|
|
132
|
+
when files are served from another address. For Cloudflare R2:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
s3Storage({
|
|
136
|
+
bucket: process.env.R2_BUCKET!,
|
|
137
|
+
region: "auto",
|
|
138
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
139
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
140
|
+
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
141
|
+
publicUrl: process.env.R2_PUBLIC_URL,
|
|
142
|
+
collections: { media: true },
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For other providers, use the settings in the adapter's README. A Node.js
|
|
147
|
+
server with a persistent disk can keep the default.
|
|
148
|
+
|
|
149
|
+
On Vercel, uploads from the admin pass through a Vercel Function, which
|
|
150
|
+
refuses request bodies over 4.5 MB (`413 FUNCTION_PAYLOAD_TOO_LARGE`). With
|
|
151
|
+
this setup, files above that size cannot be uploaded through the admin; a
|
|
152
|
+
Node.js server has no such limit.
|
|
153
|
+
|
|
154
|
+
## 6. Create the first admin before the site is public
|
|
155
|
+
|
|
156
|
+
A new production database has no admin. Until it has one, whoever opens
|
|
157
|
+
`/admin/setup` first becomes Super Admin, so create that account before anyone
|
|
158
|
+
else can reach the site. Setup then closes for good: a later attempt is
|
|
159
|
+
refused with `SETUP_COMPLETE`.
|
|
160
|
+
|
|
161
|
+
The simplest way is a production build on your own machine, pointed at the
|
|
162
|
+
production database:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
export DATABASE_URL="<the production database>"
|
|
166
|
+
npm run build # runs nextly migrate against it, then next build
|
|
167
|
+
npm start # next start, on http://localhost:3000
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Export every other variable the config reads as well. With Vercel Blob,
|
|
171
|
+
`BLOB_READ_WRITE_TOKEN` is needed to load `nextly.config.ts` at all, and the
|
|
172
|
+
build stops without it. With S3, the bucket and region are needed to load
|
|
173
|
+
it; the keys can come from the AWS credential chain instead (an IAM role, a
|
|
174
|
+
profile), so a build that succeeds says nothing about them. Where a value
|
|
175
|
+
exists only in the host's settings and cannot be brought to your machine, use
|
|
176
|
+
the protected-host path below instead.
|
|
177
|
+
|
|
178
|
+
Open `http://localhost:3000/admin/setup`, create the account, stop the
|
|
179
|
+
server, then deploy. If you cannot reach the production database from your
|
|
180
|
+
machine, keep the deployment private until setup is done, behind your host's
|
|
181
|
+
access protection.
|
|
182
|
+
|
|
183
|
+
Never run `next dev` against the production database. Development mode
|
|
184
|
+
applies schema changes directly to whatever database it points at.
|
|
185
|
+
|
|
186
|
+
## 7. Deploy
|
|
187
|
+
|
|
188
|
+
- **Vercel:** import the repository; Next.js is detected. Add the variables
|
|
189
|
+
above for Production (and Preview, with its own database), then deploy.
|
|
190
|
+
The `build` script migrates the database during the build.
|
|
191
|
+
- **Node.js server:** install from the lockfile (`npm ci`, `pnpm install
|
|
192
|
+
--frozen-lockfile`, `yarn install --immutable` on Yarn 2+, `yarn install --frozen-lockfile` on
|
|
193
|
+
Yarn 1, or `bun install --frozen-lockfile`), run the `build` script, then `next start` under a
|
|
194
|
+
process manager such as systemd, pm2 or Docker. Put a reverse proxy in
|
|
195
|
+
front that terminates TLS and forwards `Host`, `X-Forwarded-Proto` and
|
|
196
|
+
`X-Forwarded-For`.
|
|
197
|
+
|
|
198
|
+
### Behind a proxy, including Vercel
|
|
199
|
+
|
|
200
|
+
Nextly ignores `X-Forwarded-For` unless told to trust it, and then it cannot
|
|
201
|
+
tell clients apart: every sign-in, registration and password-reset request
|
|
202
|
+
shares one rate-limit bucket, 30 an hour by default, and everyone gets 429
|
|
203
|
+
after that. When every request reaches the app through a proxy that sets
|
|
204
|
+
`X-Forwarded-For` (Vercel does; configure nginx or Caddy to), trust it:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// nextly.config.ts
|
|
208
|
+
import { defineConfig } from "nextly/config";
|
|
209
|
+
|
|
210
|
+
export default defineConfig({
|
|
211
|
+
security: { trustProxy: true },
|
|
212
|
+
// collections, typescript, ...
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
When requests pass through more than one proxy, list the proxies' addresses
|
|
217
|
+
in `TRUSTED_PROXY_IPS` (comma-separated CIDRs), so the client is read from the
|
|
218
|
+
right hop. Do not turn this on for an app clients can reach directly: they
|
|
219
|
+
could then pick their own address.
|
|
220
|
+
|
|
221
|
+
## 8. Check the live site
|
|
222
|
+
|
|
223
|
+
- `npx nextly migrate:status`, run against the production `DATABASE_URL`,
|
|
224
|
+
shows nothing pending.
|
|
225
|
+
- `/admin/setup` says setup is already complete, and `/admin` signs in with
|
|
226
|
+
the account created before the deploy.
|
|
227
|
+
- Publish an entry and open a page that lists it: it appears.
|
|
228
|
+
- Upload an image in the admin, redeploy, and check it still loads.
|
|
229
|
+
- Do not use `/api/health` as an uptime probe on serverless hosts. It reports
|
|
230
|
+
503 until Nextly has started in that same process, which a fresh instance
|
|
231
|
+
that only receives probes never does. Probe a page that reads content
|
|
232
|
+
instead.
|
|
233
|
+
|
|
234
|
+
## If something fails
|
|
235
|
+
|
|
236
|
+
- **Tables missing, or `relation "dc_..." does not exist`:** the migrations
|
|
237
|
+
were not committed, or `nextly migrate` did not run against this database.
|
|
238
|
+
- **The admin and its API answer 500 in production:** check the server log.
|
|
239
|
+
`NEXTLY_SECRET of at least 32 characters is required` means the secret is
|
|
240
|
+
missing or too short, and `NEXT_PUBLIC_APP_URL is required` that the URL is
|
|
241
|
+
not set.
|
|
242
|
+
- **Sign-in fails with `CSRF_FAILED` behind a proxy:** add the public origin
|
|
243
|
+
to `NEXTLY_ALLOWED_ORIGINS`.
|
|
244
|
+
- **`next build` fails on a page reading content:** in an app with
|
|
245
|
+
`cacheComponents: true`, wrap the read in `cachedFind` (the
|
|
246
|
+
`nextly-collections` skill) or render it inside `<Suspense>`.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextly-install
|
|
3
|
+
description: >-
|
|
4
|
+
Use when adding Nextly, the open-source CMS that runs inside a Next.js app,
|
|
5
|
+
to an existing Next.js App Router project: installing its packages,
|
|
6
|
+
creating nextly.config.ts, mounting the admin panel at /admin, choosing a
|
|
7
|
+
database, and creating the first admin account. Also use when an existing
|
|
8
|
+
Nextly install shows no collections, fails to load nextly.config.ts, or its
|
|
9
|
+
admin panel misbehaves after setup.
|
|
10
|
+
metadata:
|
|
11
|
+
nextly-version: 0.0.2-alpha.67
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Install Nextly into an existing Next.js app
|
|
15
|
+
|
|
16
|
+
Nextly runs inside the app: same server, same database, no separate service.
|
|
17
|
+
The project generator detects an existing Next.js app and installs Nextly into
|
|
18
|
+
it, including the build approvals, `esbuild` and the routes the admin needs.
|
|
19
|
+
|
|
20
|
+
Checked against `nextly` 0.0.2-alpha.67 with Next.js 16.4. Run
|
|
21
|
+
`npx nextly --version` after installing; if it prints a newer version, prefer
|
|
22
|
+
<https://nextlyhq.com/docs> where it disagrees with this skill.
|
|
23
|
+
|
|
24
|
+
## Before you start
|
|
25
|
+
|
|
26
|
+
- Next.js 14, 15 or 16 with the App Router (`app/` or `src/app/`). On
|
|
27
|
+
Next.js 14 the generator leaves the Next config alone and prints the
|
|
28
|
+
`experimental` block to add instead (`serverComponentsExternalPackages` and
|
|
29
|
+
`instrumentationHook`).
|
|
30
|
+
- Node.js `^20.19.0 || ^22.12.0 || >=24.0.0`. Node 23 is not supported.
|
|
31
|
+
- A clean git working tree, so every change below is visible in one diff.
|
|
32
|
+
|
|
33
|
+
## 1. Run the generator in the project root
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx create-nextly-app@alpha # npm
|
|
37
|
+
pnpm dlx create-nextly-app@alpha # pnpm
|
|
38
|
+
yarn dlx create-nextly-app@alpha # yarn 2 and later
|
|
39
|
+
yarn create nextly-app # yarn 1 (Classic): no tag; installs `latest`, the current release
|
|
40
|
+
bunx create-nextly-app@alpha # bun
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It finds `next` in `package.json`, asks "Install Nextly in this project?", then
|
|
44
|
+
asks which database and, for PostgreSQL or MySQL, its connection string. `-y`
|
|
45
|
+
skips the questions and uses SQLite at `./data/nextly.db`. `--database
|
|
46
|
+
postgresql` or `--database mysql` picks another; with `-y` as well, `.env` gets
|
|
47
|
+
a placeholder `DATABASE_URL` to replace before starting.
|
|
48
|
+
|
|
49
|
+
It installs with the app's own package manager (inside a monorepo, the
|
|
50
|
+
workspace's), adding the packages to the app's own `package.json`. On pnpm 10
|
|
51
|
+
and later it approves the native build scripts Nextly needs (`better-sqlite3`,
|
|
52
|
+
`esbuild`, `sharp`) in the setting that pnpm version reads: `pnpm-workspace.yaml`
|
|
53
|
+
from pnpm 10.6, `package.json` before it. In a monorepo the approvals go at the
|
|
54
|
+
workspace root.
|
|
55
|
+
|
|
56
|
+
Develop on the database you will run in production. Migrations are written in
|
|
57
|
+
the dialect of the database you develop against (the `nextly-deploy` skill),
|
|
58
|
+
and SQLite is not suitable for serverless hosting.
|
|
59
|
+
|
|
60
|
+
What it changes. It edits `package.json`, the Next config and pnpm's settings
|
|
61
|
+
in place, and adds to `.env` and `.env.example` only the keys they lack. A file
|
|
62
|
+
it would create that the app already has (`nextly.config.ts`,
|
|
63
|
+
`instrumentation.ts`, the admin's files and the routes) is kept as it is and
|
|
64
|
+
reported as skipped, with what Nextly needs in it. Read its report, then review
|
|
65
|
+
the rest in `git diff`:
|
|
66
|
+
|
|
67
|
+
| File | What |
|
|
68
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `package.json` | `nextly`, `@nextlyhq/admin`, `@nextlyhq/ui`, the database adapter and driver, `@tanstack/react-query`, `lucide-react`; `esbuild` as a dev dependency |
|
|
70
|
+
| `nextly.config.ts` | the config the CMS boots from, with empty `collections` and `singles` |
|
|
71
|
+
| `next.config.ts` | Nextly, the adapter and the driver added to `serverExternalPackages` |
|
|
72
|
+
| `src/instrumentation.ts` | starts Nextly when the server starts |
|
|
73
|
+
| `src/app/admin/[[...params]]/` | the admin panel page and layout |
|
|
74
|
+
| `src/app/admin/api/[[...params]]/route.ts` | the admin API |
|
|
75
|
+
| `src/app/admin/api/media/[[...path]]/route.ts` | the admin's media route, where it uploads and edits media (signed-in only) |
|
|
76
|
+
| `src/app/api/media/[[...path]]/route.ts` | public, read-only media routes |
|
|
77
|
+
| `src/app/api/health/route.ts` | a health check |
|
|
78
|
+
| `src/types/generated/` | where generated types go |
|
|
79
|
+
| `.env`, `.env.example` | `DB_DIALECT`, `DATABASE_URL`, `NEXTLY_SECRET`, `NEXT_PUBLIC_APP_URL`, for the keys missing |
|
|
80
|
+
|
|
81
|
+
Without a `src/` folder the same files go under `app/`, and `instrumentation.ts`
|
|
82
|
+
sits at the project root.
|
|
83
|
+
|
|
84
|
+
If it reports that it left the Next config alone (a config it cannot change
|
|
85
|
+
safely, or Next.js 14), add what it prints. The list it needs holds `nextly`,
|
|
86
|
+
`@nextlyhq/adapter-drizzle`, `drizzle-orm`, `drizzle-kit`, `bcryptjs`,
|
|
87
|
+
`sharp`, `esbuild`, and your adapter and driver: `@nextlyhq/adapter-sqlite`
|
|
88
|
+
and `better-sqlite3`, `@nextlyhq/adapter-postgres` and `pg`, or
|
|
89
|
+
`@nextlyhq/adapter-mysql` and `mysql2`.
|
|
90
|
+
|
|
91
|
+
## 2. Start it and create the first admin
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run dev
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Open `http://localhost:3000/admin/setup` and create the first account; it gets
|
|
98
|
+
the Super Admin role. Nextly creates its own tables when the server starts,
|
|
99
|
+
from `instrumentation.ts`; without that hook, on the first request. Then sign in
|
|
100
|
+
at `/admin`. Keep to one address for the admin: a form posted from an origin
|
|
101
|
+
other than the one serving the request is refused, unless that origin is listed
|
|
102
|
+
in `NEXTLY_ALLOWED_ORIGINS`.
|
|
103
|
+
|
|
104
|
+
## Check it worked
|
|
105
|
+
|
|
106
|
+
- `/admin` shows the dashboard after signing in.
|
|
107
|
+
- `npx nextly --version` prints the installed version.
|
|
108
|
+
- The dev server log has no `esbuild` error, and `/api/health` answers.
|
|
109
|
+
|
|
110
|
+
## Doing it by hand
|
|
111
|
+
|
|
112
|
+
If the generator cannot be used, the manual steps are at
|
|
113
|
+
<https://nextlyhq.com/docs/getting-started/installation> (option B). They
|
|
114
|
+
include the pnpm build approvals, `esbuild` and the admin's media route, which
|
|
115
|
+
the generator otherwise adds for you.
|
|
116
|
+
|
|
117
|
+
## Next
|
|
118
|
+
|
|
119
|
+
- Define content: the `nextly-collections` skill.
|
|
120
|
+
- Who can sign in and what they can do: the `nextly-authentication` skill.
|
|
121
|
+
- Going live: the `nextly-deploy` skill.
|
|
122
|
+
- Every docs page, for agents: <https://nextlyhq.com/llms.txt>.
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
DB_DIALECT={{databaseDialect}}
|
|
6
6
|
DATABASE_URL={{databaseUrl}}
|
|
7
7
|
|
|
8
|
-
# Authentication (REQUIRED)
|
|
9
|
-
#
|
|
10
|
-
NEXTLY_SECRET=
|
|
8
|
+
# Authentication (REQUIRED): a random secret of at least 32 characters, for
|
|
9
|
+
# example from `openssl rand -base64 32`. The real one goes in .env, never here.
|
|
10
|
+
NEXTLY_SECRET=
|
|
11
11
|
|
|
12
12
|
# Where the Nextly app itself lives: the admin, the API, and the origin its
|
|
13
13
|
# emails and preview links point at.
|
|
@@ -29,5 +29,3 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
|
29
29
|
# file would be true in exactly that case, which is the one it guards against.
|
|
30
30
|
# NEXTLY_DEV_DIAGNOSTICS=1
|
|
31
31
|
|
|
32
|
-
# Storage Configuration
|
|
33
|
-
{{storageEnvVars}}
|
|
@@ -112,6 +112,8 @@ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
|
|
|
112
112
|
collection or on any of its fields?**
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
|
+
import { cachedFind, nextlyTags } from "nextly/runtime";
|
|
116
|
+
|
|
115
117
|
// NO read rule, on the collection OR its fields — the result is identical for
|
|
116
118
|
// every reader, so one shared entry is correct and gets the full ISR benefit.
|
|
117
119
|
// `find()` always returns the list envelope `{ items, meta }`, so a detail route takes the
|
|
@@ -184,6 +186,35 @@ page that does not exist. Neither is version-stamped: they describe the CURRENT
|
|
|
184
186
|
release, so check the version above before trusting a detail against an older
|
|
185
187
|
pin.
|
|
186
188
|
|
|
189
|
+
## Skills for Nextly tasks
|
|
190
|
+
|
|
191
|
+
If this project has `.agents/skills/`, it holds skills for common Nextly
|
|
192
|
+
tasks, and `.claude/skills/` holds the same files for Claude Code. Load the
|
|
193
|
+
one that matches a task before starting it. They were written for the Nextly
|
|
194
|
+
release this project was created with.
|
|
195
|
+
|
|
196
|
+
Each skill names the Nextly release it was checked against in its
|
|
197
|
+
frontmatter, as `metadata.nextly-version`. The command below installs skills
|
|
198
|
+
written for `{{skillsVersion}}`. Restore them as they were released only when
|
|
199
|
+
`.agents/skills/nextly-install/SKILL.md` exists, its `metadata.nextly-version`
|
|
200
|
+
is exactly `{{skillsVersion}}`, and `{{execCommand}} nextly --version` prints
|
|
201
|
+
exactly `{{skillsVersion}}`:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npx skills add 'nextlyhq/nextly#v{{skillsTag}}' --skill '*' --agent claude-code codex cursor --copy -y
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
It rewrites both folders, so edits made to a skill there are replaced.
|
|
208
|
+
`{{skillsTag}}` is the release of `create-nextly-app` that made this project,
|
|
209
|
+
whose tag publishes these skills. Releases up to 0.0.2-alpha.66 publish no
|
|
210
|
+
skills for building apps: installing from one of those tags brings the skills
|
|
211
|
+
Nextly's own contributors use, so never put an older version in its place.
|
|
212
|
+
|
|
213
|
+
Otherwise, with no skills or with skills for a different release than this
|
|
214
|
+
project runs, do not run the command above: it would install skills written
|
|
215
|
+
for another release. See <https://nextlyhq.com/docs/guides/coding-agents> for
|
|
216
|
+
skills that match.
|
|
217
|
+
|
|
187
218
|
## Conventions worth keeping
|
|
188
219
|
|
|
189
220
|
- Server components read content directly; do not build an API route to fetch
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nextly Generated Types
|
|
3
3
|
*
|
|
4
|
-
* This file
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* This file is generated by `nextly generate:types`, which this project runs
|
|
5
|
+
* as its `types:generate` script. Run it after every change to the schema in
|
|
6
|
+
* nextly.config.ts. It reads only that file, so collections built in the
|
|
7
|
+
* Schema Builder get types once they are moved into code
|
|
8
|
+
* (`nextly db:sync --promote <slug>`).
|
|
7
9
|
*
|
|
8
10
|
* Do not edit this file manually - it will be overwritten.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
// Types
|
|
13
|
+
// Types are generated here by `nextly generate:types`.
|
|
12
14
|
export {};
|
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect,
|
|
3
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
4
4
|
|
|
5
5
|
type Theme = "light" | "dark" | "system";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
|
12
|
-
if (savedTheme) {
|
|
13
|
-
setTheme(savedTheme);
|
|
14
|
-
applyTheme(savedTheme);
|
|
15
|
-
} else {
|
|
16
|
-
applyTheme("system");
|
|
17
|
-
}
|
|
18
|
-
}, []);
|
|
7
|
+
const STORAGE_KEY = "theme";
|
|
8
|
+
const THEMES: readonly Theme[] = ["light", "dark", "system"];
|
|
9
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Everything showing the theme in this tab. The browser's `storage` event
|
|
13
|
+
* only reaches other tabs, so a change made here is announced here by hand.
|
|
14
|
+
*/
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
function subscribe(listener: () => void) {
|
|
18
|
+
listeners.add(listener);
|
|
19
|
+
window.addEventListener("storage", listener);
|
|
20
|
+
return () => {
|
|
21
|
+
listeners.delete(listener);
|
|
22
|
+
window.removeEventListener("storage", listener);
|
|
27
23
|
};
|
|
24
|
+
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
/** The saved choice, or "system" when there is none or it is not one of the three. */
|
|
27
|
+
function readTheme(): Theme {
|
|
28
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
return THEMES.find(theme => theme === saved) ?? "system";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveTheme(theme: Theme) {
|
|
33
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
34
|
+
for (const listener of listeners) listener();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyTheme(theme: Theme) {
|
|
38
|
+
const isDark =
|
|
39
|
+
theme === "dark" ||
|
|
40
|
+
(theme === "system" && window.matchMedia(DARK_QUERY).matches);
|
|
41
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ThemeToggle() {
|
|
45
|
+
// Read from storage rather than copied into state by an effect. The server
|
|
46
|
+
// renders "system"; after hydration React swaps in the saved choice.
|
|
47
|
+
const theme = useSyncExternalStore<Theme>(
|
|
48
|
+
subscribe,
|
|
49
|
+
readTheme,
|
|
50
|
+
() => "system"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Keep the page's colours in step with the choice, and with the system's
|
|
54
|
+
// setting for as long as "system" is the choice.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
applyTheme(theme);
|
|
57
|
+
if (theme !== "system") return;
|
|
58
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
59
|
+
const follow = () => applyTheme("system");
|
|
60
|
+
query.addEventListener("change", follow);
|
|
61
|
+
return () => query.removeEventListener("change", follow);
|
|
62
|
+
}, [theme]);
|
|
63
|
+
|
|
64
|
+
const handleThemeChange = (newTheme: Theme) => saveTheme(newTheme);
|
|
34
65
|
|
|
35
66
|
return (
|
|
36
67
|
<div className="fixed top-6 right-6 flex items-center border border-foreground/10 bg-background p-1 z-50">
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts Nextly when the server starts, before it serves a request.
|
|
3
|
+
*
|
|
4
|
+
* Next.js runs `register()` once per server instance and finishes it before
|
|
5
|
+
* the instance handles traffic, in every serverless function as well as the
|
|
6
|
+
* admin's. Without it, Nextly starts on the first request that happens to
|
|
7
|
+
* need it, so a route that cannot start it alone, such as `/api/health`,
|
|
8
|
+
* answers as if the server could not serve until some other request has.
|
|
9
|
+
*
|
|
10
|
+
* It also registers Nextly's Next.js cache adapter here, so a Direct API write
|
|
11
|
+
* from any function flushes the cache tags it touches.
|
|
12
|
+
*
|
|
13
|
+
* Guarded to the Node.js runtime and imported dynamically inside the guard:
|
|
14
|
+
* Nextly needs Node-only packages that the Edge runtime cannot bundle. Next.js
|
|
15
|
+
* skips `register()` during `next build`, so this never opens a database
|
|
16
|
+
* connection at build time.
|
|
17
|
+
*/
|
|
18
|
+
export async function register(): Promise<void> {
|
|
19
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
20
|
+
|
|
21
|
+
const { registerNextCacheRevalidator } = await import("nextly/runtime");
|
|
22
|
+
registerNextCacheRevalidator();
|
|
23
|
+
|
|
24
|
+
const { createRegister } = await import("nextly");
|
|
25
|
+
const { default: config } = await import("../nextly.config");
|
|
26
|
+
await createRegister(config)();
|
|
27
|
+
}
|
|
@@ -40,10 +40,10 @@ working on an app.
|
|
|
40
40
|
|
|
41
41
|
- **Plugin APIs come from `@nextlyhq/plugin-sdk`; schema APIs come from
|
|
42
42
|
`nextly/config`.** The SDK is the stability boundary for everything a plugin
|
|
43
|
-
does AS a plugin: `definePlugin`, `
|
|
44
|
-
`
|
|
45
|
-
from `@nextlyhq/plugin-sdk/
|
|
46
|
-
|
|
43
|
+
does AS a plugin: `definePlugin`, field factories, `pluginField` and
|
|
44
|
+
`NextlyError` from `@nextlyhq/plugin-sdk`; `registerComponents` and
|
|
45
|
+
`registerKnownPlugin` from `@nextlyhq/plugin-sdk/admin`; `createTestNextly`
|
|
46
|
+
from `@nextlyhq/plugin-sdk/testing`. Collections and singles are defined the same way an app defines
|
|
47
47
|
them, with `defineCollection` from `nextly/config` — which is what
|
|
48
48
|
`src/collections/example.ts` does.
|
|
49
49
|
|
|
@@ -73,6 +73,8 @@ in-memory SQLite with your plugin registered, so a test exercises the same
|
|
|
73
73
|
lifecycle a host would run:
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
+
import { createTestNextly } from "@nextlyhq/plugin-sdk/testing";
|
|
77
|
+
|
|
76
78
|
const t = await createTestNextly({ plugins: [myPlugin()] });
|
|
77
79
|
```
|
|
78
80
|
|
|
@@ -13,6 +13,10 @@ Your plugin lives in `src/`. The `dev/` folder is a minimal Nextly app on SQLite
|
|
|
13
13
|
that registers this plugin so you can exercise it in a real admin with hot-reload.
|
|
14
14
|
Editing files under `src/` reloads the playground. **`dev/` is never published.**
|
|
15
15
|
|
|
16
|
+
The playground signs you in automatically as `dev@nextly.local` (password
|
|
17
|
+
`DevPassword123!`). That user is created on the first `pnpm dev`, and only
|
|
18
|
+
under `next dev`; production builds ignore the auto-login setting.
|
|
19
|
+
|
|
16
20
|
## Test
|
|
17
21
|
|
|
18
22
|
```bash
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
# Dev playground env.
|
|
1
|
+
# Dev playground env. create-nextly-app writes dev/.env from this file;
|
|
2
|
+
# copy it again to start over.
|
|
2
3
|
DB_DIALECT=sqlite
|
|
3
|
-
|
|
4
|
+
# Relative to the plugin root, where `pnpm dev` runs (`next dev dev` keeps
|
|
5
|
+
# that working directory), so the database lands in dev/.
|
|
6
|
+
DATABASE_URL=file:./dev/nextly-dev.db
|
|
4
7
|
|
|
5
8
|
# Any non-empty secret works for local dev (sign/verify session cookies).
|
|
6
9
|
NEXTLY_SECRET=dev-secret-change-me
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import "@nextlyhq/admin/style.css";
|
|
4
|
-
import { RootLayout } from "@nextlyhq/admin";
|
|
4
|
+
import { ErrorBoundary, QueryProvider, RootLayout } from "@nextlyhq/admin";
|
|
5
5
|
|
|
6
6
|
// Mounts the Nextly admin shell. Your plugin's menu/pages/settings appear here.
|
|
7
|
+
// The admin's data hooks read their client from QueryProvider, so RootLayout
|
|
8
|
+
// renders only inside it, as in an app scaffold.
|
|
7
9
|
export default function AdminPage() {
|
|
8
|
-
return
|
|
10
|
+
return (
|
|
11
|
+
<ErrorBoundary
|
|
12
|
+
onError={(error, errorInfo) => {
|
|
13
|
+
console.error("Admin error:", error, errorInfo);
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<QueryProvider>
|
|
17
|
+
<RootLayout />
|
|
18
|
+
</QueryProvider>
|
|
19
|
+
</ErrorBoundary>
|
|
20
|
+
);
|
|
9
21
|
}
|