create-smoodly-app 0.0.35 → 0.0.37
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 +1 -200
- package/package.json +1 -1
- package/templates/blank/app/(site)/layout.tsx +2 -2
- package/templates/blank/components/pages/Home.tsx +1 -1
- package/templates/blank/components/pages/Page.tsx +1 -1
- package/templates/blank/components/pages/Post.tsx +5 -7
- package/templates/blank/package.json +1 -1
- package/templates/blank/smoodly/admin.tsx +2 -2
- package/templates/blank/smoodly/server.ts +3 -3
- package/templates/blank/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/blank/supabase/migrations/{20260929135623_smoodly_views.sql → 20260930041056_smoodly_views.sql} +3 -6
- package/templates/blank-intl/app/[locale]/layout.tsx +2 -2
- package/templates/blank-intl/components/pages/Home.tsx +1 -1
- package/templates/blank-intl/components/pages/Page.tsx +1 -1
- package/templates/blank-intl/components/pages/Post.tsx +5 -7
- package/templates/blank-intl/package.json +1 -1
- package/templates/blank-intl/smoodly/admin.tsx +2 -2
- package/templates/blank-intl/smoodly/server.ts +3 -3
- package/templates/blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/blank-intl/supabase/migrations/{20260929135623_smoodly_views.sql → 20260930041056_smoodly_views.sql} +3 -6
- package/templates/lorem/app/(site)/layout.tsx +2 -2
- package/templates/lorem/components/PageFrame.tsx +1 -1
- package/templates/lorem/components/articles.ts +0 -1
- package/templates/lorem/components/news.ts +0 -1
- package/templates/lorem/components/pages/Article.tsx +32 -14
- package/templates/lorem/components/pages/Articles.tsx +4 -6
- package/templates/lorem/components/pages/Home.tsx +1 -1
- package/templates/lorem/components/pages/News.tsx +8 -12
- package/templates/lorem/components/pages/NewsPost.tsx +6 -6
- package/templates/lorem/components/pages/Page.tsx +1 -1
- package/templates/lorem/components/sections/FavouriteArticles.tsx +2 -3
- package/templates/lorem/components/sections/People.tsx +1 -1
- package/templates/lorem/messages/en.json +1 -0
- package/templates/lorem/package.json +1 -1
- package/templates/lorem/smoodly/admin.tsx +2 -2
- package/templates/lorem/smoodly/collections.ts +2 -0
- package/templates/lorem/smoodly/server.ts +3 -3
- package/templates/lorem/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/lorem/supabase/migrations/{20260929135623_smoodly_views.sql → 20260930041056_smoodly_views.sql} +4 -10
- package/templates/lorem-intl/app/[locale]/layout.tsx +2 -2
- package/templates/lorem-intl/components/PageFrame.tsx +1 -1
- package/templates/lorem-intl/components/articles.ts +0 -1
- package/templates/lorem-intl/components/news.ts +0 -1
- package/templates/lorem-intl/components/pages/Article.tsx +32 -14
- package/templates/lorem-intl/components/pages/Articles.tsx +4 -6
- package/templates/lorem-intl/components/pages/Home.tsx +1 -1
- package/templates/lorem-intl/components/pages/News.tsx +8 -12
- package/templates/lorem-intl/components/pages/NewsPost.tsx +6 -6
- package/templates/lorem-intl/components/pages/Page.tsx +1 -1
- package/templates/lorem-intl/components/sections/FavouriteArticles.tsx +2 -3
- package/templates/lorem-intl/components/sections/People.tsx +1 -1
- package/templates/lorem-intl/messages/en.json +1 -0
- package/templates/lorem-intl/messages/fi.json +1 -0
- package/templates/lorem-intl/package.json +1 -1
- package/templates/lorem-intl/smoodly/admin.tsx +2 -2
- package/templates/lorem-intl/smoodly/collections.ts +2 -0
- package/templates/lorem-intl/smoodly/server.ts +3 -3
- package/templates/lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +84 -0
- package/templates/lorem-intl/supabase/migrations/{20260929135623_smoodly_views.sql → 20260930041056_smoodly_views.sql} +4 -10
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A fresh Next.js app with the Smoodly admin mounted at `/admin`, a Supabase
|
|
6
6
|
project holding the schema, `.env.local` written, and a first editor who can
|
|
7
|
-
sign in.
|
|
7
|
+
sign in.
|
|
8
8
|
|
|
9
9
|
## What it asks
|
|
10
10
|
|
|
@@ -41,202 +41,3 @@ Every prompt has a flag, so CI can run it: `--supabase local|hosted`, `--locales
|
|
|
41
41
|
`SMOODLY_EDITOR_PASSWORD`), `--skip-link`, `--yes`. `--help` lists them.
|
|
42
42
|
A hidden `--smoodly-dep <spec>` overrides the `smoodly` dependency written into
|
|
43
43
|
`package.json`; CI points it at the packed tarball.
|
|
44
|
-
|
|
45
|
-
## How it is built
|
|
46
|
-
|
|
47
|
-
`templates/lorem`, `templates/lorem-intl`, `templates/blank` and
|
|
48
|
-
`templates/blank-intl` are all generated by `scripts/build-template.ts`.
|
|
49
|
-
The Lorem Co. pair comes from `examples/site` — the multilingual layout —
|
|
50
|
-
plus the five single-language files under `variants/lorem/single/`. The
|
|
51
|
-
blank pair (spec 2026-09-11) takes only the content-free glue from the
|
|
52
|
-
example (`smoodly/server.ts`, `ops.ts`, `admin.tsx`, `richtext.tsx`, the
|
|
53
|
-
admin route files, next-intl's files) and everything with content from
|
|
54
|
-
`variants/blank/{common,single,intl}/`. Each template also gets a generated
|
|
55
|
-
`package.json` (with `smoodly` pinned to this package's version),
|
|
56
|
-
`tsconfig`, `next.config`, README, `AGENTS.md`, a `CLAUDE.md` that imports
|
|
57
|
-
it, and the one migration (`coreTablesSQL` + the starter's collection
|
|
58
|
-
views). Two exact-line transforms turn the example's glue (locale config,
|
|
59
|
-
`href`) into the single-language one; a drifted example fails the build
|
|
60
|
-
rather than silently shipping stale copies. Every template gets a check
|
|
61
|
-
tsconfig beside it; `npm run typecheck` runs `tsc` over all four
|
|
62
|
-
(`typecheck:templates`), so hand-kept files cannot drift silently. All
|
|
63
|
-
four templates are git-ignored and rebuilt by `npm run build`.
|
|
64
|
-
|
|
65
|
-
`src/plan.ts` turns answers into steps; `src/run.ts` executes them with
|
|
66
|
-
injected `exec`, fs and Supabase client so the sequence and every fallback
|
|
67
|
-
message are unit-tested. `src/prompts.ts` is the only interactive code.
|
|
68
|
-
The CLI has no runtime dependency on `smoodly`.
|
|
69
|
-
|
|
70
|
-
npm test # unit tests (template build, planner, runner, editor)
|
|
71
|
-
npm run typecheck # tsc, then the four templates
|
|
72
|
-
npm run build # templates + esbuild bundle → dist/index.js
|
|
73
|
-
|
|
74
|
-
Walked 2026-09-06: both templates scaffolded against the repo's local stack
|
|
75
|
-
in hosted mode, `tsc`, `next build` and `/admin` 200 for each; the
|
|
76
|
-
multilingual one with `--locales en,de`.
|
|
77
|
-
|
|
78
|
-
Walked 2026-09-11: blank multilingual scaffolded in local mode with
|
|
79
|
-
`--locales en,fr` from the built CLI, `next build`, `/` and `/fr` seeded
|
|
80
|
-
with the Text section and the site footer line, the admin listing Home,
|
|
81
|
-
Posts and the Site item, a post with a Button element published and
|
|
82
|
-
served at `/posts/<slug>` in both locales; blank single-language
|
|
83
|
-
scaffolded in hosted mode against the repo stack and `next build` only.
|
|
84
|
-
|
|
85
|
-
## Follow-ups (logged 2026-09-28, fresh-install proof of 0.0.30)
|
|
86
|
-
|
|
87
|
-
Walked 2026-09-28: `npx create-smoodly-app@0.0.30 proof-0030 --supabase
|
|
88
|
-
local --yes --editor-email …` from the published package, in a scratch
|
|
89
|
-
directory beside three running stacks (the new one took the `563xx`
|
|
90
|
-
block). `smoodly` 0.0.30 installed, `tsc` and `next build` clean, `/`,
|
|
91
|
-
`/posts`, `/posts/<slug>`, `/company/about-us` and `/admin` 200, the
|
|
92
|
-
admin document's `<html lang="en">` present, the editor signed in, an
|
|
93
|
-
SEO title edit saved and published and served on `/`. Found:
|
|
94
|
-
|
|
95
|
-
- **Local mode left Realtime out (0.0.26–0.0.30).** `LOCAL_START_ARGS`
|
|
96
|
-
excluded `realtime` from before the concurrency spec, whose presence
|
|
97
|
-
channel and `saved` events ride it; the admin retried the WebSocket
|
|
98
|
-
forever (a console error per attempt) and a local install had no
|
|
99
|
-
presence or stale-page notice. CI's repo stack already kept Realtime.
|
|
100
|
-
Fixed 2026-09-28: the list no longer excludes it; restarted with the new
|
|
101
|
-
flags, a script signed in as the editor joined `space:<id>` and saw both
|
|
102
|
-
open admin tabs tracked on Home. The live job does not catch this
|
|
103
|
-
(it requests `/admin` once, no browser); an existing local install
|
|
104
|
-
restarts with `supabase stop && supabase start -x imgproxy,edge-runtime,logflare,vector,supavisor`.
|
|
105
|
-
- **The Lorem home's custom-section CTA linked `/articles`**, a 404 —
|
|
106
|
-
the articles collection lives at `/posts`. `examples/site/scripts/seed.ts`
|
|
107
|
-
hardcoded the href; fixed 2026-09-28 to `paths(locale).articles` like
|
|
108
|
-
every other starter link (Finnish gets its own segment too).
|
|
109
|
-
- **`--editor-email` without `SMOODLY_EDITOR_PASSWORD` fails late.** The
|
|
110
|
-
check lives in `askEditorWith`, which runs after the copy, install,
|
|
111
|
-
`git init` and `supabase start`, so the run stops at `ensureEditor`
|
|
112
|
-
with a half-made app. It has to be late for hosted mode (the email may
|
|
113
|
-
name an existing Auth user, no password needed), but a local stack is
|
|
114
|
-
always fresh, so local mode could refuse up front. Fixed 2026-09-28:
|
|
115
|
-
`resolveAnswers` refuses local `--editor-email` without the variable
|
|
116
|
-
before anything is written (the built CLI exits 1, no directory made);
|
|
117
|
-
hosted keeps the late check.
|
|
118
|
-
|
|
119
|
-
## Follow-ups (logged 2026-09-25, docs walk)
|
|
120
|
-
|
|
121
|
-
- **Single-language `/admin` had no root `<html>` (0.0.15–0.0.20).** The
|
|
122
|
-
variant's `app/(smoodly)/admin/layout.tsx` re-exported `AdminLayout`,
|
|
123
|
-
which renders a `<div>`; that only worked while the site had a
|
|
124
|
-
top-level `app/layout.tsx`, and 0.0.15 moved the site into `app/(site)`.
|
|
125
|
-
`next dev` showed two errors on the first visit to `/admin`: "Missing
|
|
126
|
-
<html> tags in the root layout" and, from the client-rendered tree, the
|
|
127
|
-
theme boot's "Encountered a script tag while rendering React component".
|
|
128
|
-
Found walking the Getting started guide on a fresh `lorem` install of
|
|
129
|
-
0.0.20. Fixed 2026-09-25: the variant layout wraps `AdminLayout` in
|
|
130
|
-
`<html><body>` like the example's; both errors gone in the walk. CI's
|
|
131
|
-
live job only checks `/admin` for a 200, which this passed; an assertion
|
|
132
|
-
on the dev overlay (or on `<html>` in the served document) would have
|
|
133
|
-
caught it.
|
|
134
|
-
|
|
135
|
-
## Follow-ups (logged 2026-09-11)
|
|
136
|
-
|
|
137
|
-
- **Blank single-language is typechecked, never built by CI.** The live
|
|
138
|
-
job scaffolds lorem single (hosted), lorem intl and blank intl (local).
|
|
139
|
-
A fourth stack costs ~2 minutes; add it if blank single ever breaks in
|
|
140
|
-
a way `tsc` cannot see (a runtime import, a missing file at a path
|
|
141
|
-
Next reads).
|
|
142
|
-
- **The guide inside the package.** `AGENTS.md` is generated per scaffold
|
|
143
|
-
and frozen at install time. Once its text settles, ship it in the
|
|
144
|
-
`smoodly` package (`smoodly/AGENTS.md`) and have the scaffold's copy
|
|
145
|
-
point there, so it tracks the package version.
|
|
146
|
-
|
|
147
|
-
## Follow-ups (logged 2026-09-06)
|
|
148
|
-
|
|
149
|
-
- Local mode on a fresh multilingual install (0.0.5, 2026-09-06) failed at
|
|
150
|
-
`supabase start` with `supabase_storage_<app> container is not ready:
|
|
151
|
-
unhealthy` — the storage container logged `Started Successfully` and the
|
|
152
|
-
CLI still stopped the stack. Five local stacks (57 containers) were
|
|
153
|
-
sharing 7.8 GB of Docker memory at the time, the same pressure that
|
|
154
|
-
made the repo's live suites flake on 2026-09-05. Smoodly never uses
|
|
155
|
-
storage, so the CLI could start the app's stack with
|
|
156
|
-
`supabase start -x storage-api,imgproxy,edge-runtime,logflare,vector,realtime`
|
|
157
|
-
(the container names `supabase start --help` lists; CI already excludes
|
|
158
|
-
most of these for the repo stack), and the "Finish by hand" text could
|
|
159
|
-
mention `supabase stop` on other projects. Retried the same day with
|
|
160
|
-
the other four stacks stopped (`supabase stop --project-id <name>`):
|
|
161
|
-
`npx create-smoodly-app@0.0.5 --supabase local --locales en,fi --yes`
|
|
162
|
-
came up with every container healthy, storage included, and the app
|
|
163
|
-
built and served `/admin` 200, `/` and `/fi` 404 — so the cause is
|
|
164
|
-
memory pressure, not the CLI. Fixed the same day: `run.ts` starts the
|
|
165
|
-
app's stack with `-x storage-api,imgproxy,edge-runtime,logflare,vector,realtime,supavisor`
|
|
166
|
-
(`LOCAL_START_ARGS`), and the "Finish by hand" text carries the same
|
|
167
|
-
flags. Kept: db, kong, auth, PostgREST, Studio and pg-meta, mailpit.
|
|
168
|
-
Storage is back in the local stack since the media library
|
|
169
|
-
(2026-09-08); the health-check flake is a Docker memory problem, see
|
|
170
|
-
the repo's memory note.
|
|
171
|
-
- **A non-interactive run without `--locales` needs `--yes`.** The
|
|
172
|
-
"Multilingual site?" prompt is asked whenever `--locales` is absent, and
|
|
173
|
-
with no TTY (a CI step, stdin at EOF) the CLI exits 13 having created
|
|
174
|
-
nothing — "Detected unsettled top-level await". CI's hosted-mode e2e step
|
|
175
|
-
hit this; both e2e steps now pass a flag that settles the question
|
|
176
|
-
(`--yes` for single-language, `--locales en,de` for the multilingual one).
|
|
177
|
-
A prompt that treated EOF as its default value would need no flag.
|
|
178
|
-
- **The next-intl template inherits next-intl's `localeDetection`
|
|
179
|
-
default**, which redirects an unprefixed URL to the visitor's preferred
|
|
180
|
-
locale *keeping the source locale's slugs* — on a site with translated
|
|
181
|
-
slugs that redirect lands on a 404 (`/company/about-us` →
|
|
182
|
-
`/fi/company/about-us` for a Finnish browser). Recorded in
|
|
183
|
-
`examples/site/README.md` under "Multilingual (2026-09-06)". Fixed the
|
|
184
|
-
same day: `i18n/routing.ts` in the example — and so in the generated
|
|
185
|
-
`templates/next-intl` — sets `localeDetection: false`, so the English
|
|
186
|
-
URL stays English for a Finnish browser and the visitor picks a
|
|
187
|
-
language through the site's own switcher.
|
|
188
|
-
|
|
189
|
-
## Follow-ups (logged 2026-09-05)
|
|
190
|
-
|
|
191
|
-
- Text prompts drop `@` when the terminal sends Option as Meta (macOS,
|
|
192
|
-
Finnish/Nordic layouts put `@` on Option+2): the terminal emits `ESC 2` and
|
|
193
|
-
Node's readline discards meta-keys, so nothing reaches the prompt.
|
|
194
|
-
Reproduced against `@clack/prompts` 1.7.0 with a scripted stdin. Not
|
|
195
|
-
fixable from prompt options; documented under "What it asks" with the
|
|
196
|
-
paste / `--editor-email` workaround. Revisit if clack exposes a key hook.
|
|
197
|
-
- Fresh installs printed a `NOTICE ... does not exist, skipping` for every
|
|
198
|
-
`drop trigger/policy if exists` in the migration (fixed 2026-09-05: the
|
|
199
|
-
migration opens with `set client_min_messages = warning`).
|
|
200
|
-
|
|
201
|
-
## Follow-ups (logged 2026-09-04)
|
|
202
|
-
|
|
203
|
-
- The port probe binds `0.0.0.0` (a loopback-only bind coexists with a Docker-published port on macOS), so macOS may prompt once to allow `node` incoming connections during local mode; either answer is fine.
|
|
204
|
-
- Windows: `exec` passes `shell: true` on win32 (needed for the `.cmd` shims)
|
|
205
|
-
but does not quote arguments, so a project name or path containing a space
|
|
206
|
-
or `&` breaks. Quote the args when the shell is on, or resolve the `.cmd`
|
|
207
|
-
path and drop `shell`. Untested on Windows.
|
|
208
|
-
- Hosted mode, manual run against a real hosted project with new-format keys
|
|
209
|
-
(done 2026-09-05, throwaway project, `sb_secret_` + `sb_publishable_`):
|
|
210
|
-
`db push` of the template migration printed no NOTICE, the CLI listed and
|
|
211
|
-
created the first editor through the GoTrue admin API with the secret key
|
|
212
|
-
as the sole `apikey`, the `members` row landed with role `admin`, `next
|
|
213
|
-
build` passed, `/admin` served the shell, and a password sign-in with the
|
|
214
|
-
publishable key returned a session. Caveat: the link step ran by hand with
|
|
215
|
-
an explicit `--project-ref` (see the next-but-one item); the CLI's own
|
|
216
|
-
`supabase link` was not driven interactively.
|
|
217
|
-
- Supabase CLI 2.78.1 prints `sb_secret_` keys masked (middle dots) from
|
|
218
|
-
`supabase projects api-keys`; 2.116.0 needs `--reveal`. A user who copies
|
|
219
|
-
the key from the CLI rather than the dashboard gets "Invalid API key" at
|
|
220
|
-
the editor step. Worth a hint in the "Finish by hand" text.
|
|
221
|
-
- `listUsers` reads one page of 200 Auth users when offering an existing user
|
|
222
|
-
to make the first editor. Fine for a fresh project, wrong for a populated one.
|
|
223
|
-
- `supabase link` ran without `--project-ref`, so the CLI opened the
|
|
224
|
-
project picker, which preselects the account's first project; `db push`
|
|
225
|
-
follows the link, so one Enter would push Smoodly's schema into whichever
|
|
226
|
-
project sorts first (seen 2026-09-05 during the hosted run, caught before
|
|
227
|
-
the push). Fixed 2026-09-05: `projectRef` (src/env.ts) takes the ref from
|
|
228
|
-
the typed URL's first host label and the link step and every fallback
|
|
229
|
-
line pass it. Seen again 2026-09-05 from the published 0.0.3: a URL typed
|
|
230
|
-
without `https://` gave no ref (the picker opened) and then failed
|
|
231
|
-
`ensureEditor` with supabase-js's "Invalid supabaseUrl". Fixed the same
|
|
232
|
-
day: `normalizeProjectUrl` (src/env.ts) adds the scheme and refuses
|
|
233
|
-
non-URLs at the prompt and for `--url`; a URL with no ref (custom domain)
|
|
234
|
-
never offers the link, and `run` throws rather than run a bare
|
|
235
|
-
`supabase link`.
|
|
236
|
-
- The `--yes` flag defaults to local mode; with the Supabase CLI absent that
|
|
237
|
-
stops with the install message rather than falling through to hosted.
|
|
238
|
-
- Captured `exec` output is stdout only; stderr streams straight through, so
|
|
239
|
-
`git init` hints and the Supabase CLI's update notice appear between the
|
|
240
|
-
prompt lines.
|
|
241
|
-
- The "Local" option is still offered in the select when the Supabase CLI is
|
|
242
|
-
absent, and only bails afterwards; the spec wants the check before the prompt.
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import "../globals.css";
|
|
|
11
11
|
// absolute; SITE_URL is the deployed address (spec 2026-09-29 pre-install fixes §6).
|
|
12
12
|
export async function generateMetadata() {
|
|
13
13
|
const site = await q().getGlobal("site");
|
|
14
|
-
return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.name ?? "Smoodly site" };
|
|
14
|
+
return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.fields.name ?? "Smoodly site" };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
@@ -20,7 +20,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
20
20
|
<html lang="en">
|
|
21
21
|
<body>
|
|
22
22
|
<main>{children}</main>
|
|
23
|
-
{site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
|
|
23
|
+
{site && <footer className="container site-footer">{site.fields.name} · {site.fields.tagline}</footer>}
|
|
24
24
|
</body>
|
|
25
25
|
</html>
|
|
26
26
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { smoodly
|
|
1
|
+
import { smoodly } from "smoodly";
|
|
2
2
|
import { RichText } from "@/smoodly/richtext";
|
|
3
3
|
import { posts } from "@/smoodly/collections";
|
|
4
4
|
import { LocaleNav } from "../LocaleNav";
|
|
@@ -6,10 +6,9 @@ import { LocaleNav } from "../LocaleNav";
|
|
|
6
6
|
// The entry page for `posts`, served at /posts/<slug>. It receives the
|
|
7
7
|
// entry's fields; the body is richtext rendered by the site's bound
|
|
8
8
|
// RichText, which knows the registered elements.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const post = entry.fields as PostFields;
|
|
9
|
+
export const PostPage = smoodly.entryPage(posts, function PostView({ entry }) {
|
|
10
|
+
const { locale, locales } = entry;
|
|
11
|
+
const post = entry.fields;
|
|
13
12
|
return (
|
|
14
13
|
<>
|
|
15
14
|
<LocaleNav locale={locale} locales={locales} />
|
|
@@ -20,6 +19,5 @@ function PostView({ entry, locale, locales }: EntryPageProps) {
|
|
|
20
19
|
</article>
|
|
21
20
|
</>
|
|
22
21
|
);
|
|
23
|
-
}
|
|
22
|
+
});
|
|
24
23
|
|
|
25
|
-
export const PostPage = smoodly.entryPage(posts, PostView);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSmoodlyAdmin } from "smoodly/admin/next";
|
|
2
2
|
import { config } from "./config";
|
|
3
3
|
import { smoodlyOp } from "./ops";
|
|
4
|
-
import { publicAuth,
|
|
4
|
+
import { publicAuth, presence } from "./server";
|
|
5
5
|
|
|
6
|
-
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth,
|
|
6
|
+
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth, presence });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// admin route enters it from config.ts — so nothing here may read the
|
|
10
10
|
// config at module scope.
|
|
11
11
|
import {
|
|
12
|
-
createSmoodlyClient, createSmoodlySite,
|
|
12
|
+
createSmoodlyClient, createSmoodlySite, presenceConfig, smoodlyEnv, SupabaseAssetStore, SupabaseEntryStore, SupabaseMemberStore, SupabasePageStore, SupabasePathIndex, type PresenceConfig, type SmoodlySite,
|
|
13
13
|
} from "smoodly";
|
|
14
14
|
// The admin gate from `smoodly/admin`, which holds no client code: the site
|
|
15
15
|
// routes import this file, and `smoodly/admin/next` would put the admin
|
|
@@ -89,8 +89,8 @@ export function publicAuth(): AuthConfig {
|
|
|
89
89
|
|
|
90
90
|
/** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
|
|
91
91
|
* The space is asked of the database once per process. */
|
|
92
|
-
export function
|
|
93
|
-
return
|
|
92
|
+
export function presence(): Promise<PresenceConfig> {
|
|
93
|
+
return presenceConfig(smoodlyEnv());
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Locale-relative CMS path → site URL: the one seam for the renderer
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- Smoodly core schema, version 7 (docs/superpowers/specs/2026-09-29-content-api-contract-design.md §5).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 6: smoodly_list_entries, which filters, orders and
|
|
5
|
+
-- limits a collection over the snapshot the reader is shown, and the read
|
|
6
|
+
-- policies of every content table admit a write key only (§8).
|
|
7
|
+
|
|
8
|
+
-- The shown snapshot of one collection's entries in a locale, filtered,
|
|
9
|
+
-- ordered and limited in one query (spec 2026-09-29 content API contract
|
|
10
|
+
-- §5): the published snapshot, or the live merge in preview. Invoker
|
|
11
|
+
-- rights, so RLS decides what a key sees; a preview read needs the
|
|
12
|
+
-- service role or a write key, anything else reads published.
|
|
13
|
+
create or replace function "smoodly_list_entries"(
|
|
14
|
+
p_collection text, p_locale text, p_draft boolean, p_where jsonb, p_order jsonb,
|
|
15
|
+
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
16
|
+
) returns table ("id" uuid, "fields" jsonb) language sql stable set search_path = public as $$
|
|
17
|
+
with mode as (
|
|
18
|
+
select p_draft and (current_user = 'service_role' or "smoodly_key_kind"() = 'write') as draft
|
|
19
|
+
), shown as (
|
|
20
|
+
select e."id", e."sort", e."created_at", l."updated_at",
|
|
21
|
+
case when (select draft from mode) then e."fields" || l."fields" || jsonb_build_object('slug', l."slug")
|
|
22
|
+
else coalesce(v."fields", e."fields" || l."fields" || jsonb_build_object('slug', l."slug")) end as "fields"
|
|
23
|
+
from "entries" e
|
|
24
|
+
join "entry_locales" l on l."entry_id" = e."id" and l."locale" = p_locale
|
|
25
|
+
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
26
|
+
where e."collection" = p_collection
|
|
27
|
+
and ((select draft from mode) or l."status" = 'published')
|
|
28
|
+
and (not p_has_parent or e."parent_id" is not distinct from p_parent)
|
|
29
|
+
and (p_ids is null or e."id" = any(p_ids))
|
|
30
|
+
)
|
|
31
|
+
select s."id", s."fields" from shown s
|
|
32
|
+
where not exists (
|
|
33
|
+
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
34
|
+
where not (case when w.value ? 'in'
|
|
35
|
+
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
36
|
+
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
37
|
+
order by
|
|
38
|
+
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
39
|
+
case when p_order ->> 'direction' = 'asc' then
|
|
40
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
41
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
42
|
+
case when p_order ->> 'direction' = 'desc' then
|
|
43
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
44
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
45
|
+
s."created_at" asc, s."id" asc
|
|
46
|
+
limit p_limit;
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
-- No public content reads (spec 2026-09-29 content API contract §8): the
|
|
50
|
+
-- publishable key and a read key read nothing; a write key reads its own space.
|
|
51
|
+
drop policy if exists "smoodly_read" on "pages";
|
|
52
|
+
create policy "smoodly_read" on "pages" for select
|
|
53
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
54
|
+
|
|
55
|
+
drop policy if exists "smoodly_read" on "page_locales";
|
|
56
|
+
create policy "smoodly_read" on "page_locales" for select
|
|
57
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
58
|
+
|
|
59
|
+
drop policy if exists "smoodly_read" on "page_versions";
|
|
60
|
+
create policy "smoodly_read" on "page_versions" for select
|
|
61
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
62
|
+
|
|
63
|
+
drop policy if exists "smoodly_read" on "entries";
|
|
64
|
+
create policy "smoodly_read" on "entries" for select
|
|
65
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
66
|
+
|
|
67
|
+
drop policy if exists "smoodly_read" on "entry_locales";
|
|
68
|
+
create policy "smoodly_read" on "entry_locales" for select
|
|
69
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
70
|
+
|
|
71
|
+
drop policy if exists "smoodly_read" on "entry_versions";
|
|
72
|
+
create policy "smoodly_read" on "entry_versions" for select
|
|
73
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
74
|
+
|
|
75
|
+
drop policy if exists "smoodly_read" on "paths";
|
|
76
|
+
create policy "smoodly_read" on "paths" for select
|
|
77
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
78
|
+
|
|
79
|
+
drop policy if exists "smoodly_read" on "assets";
|
|
80
|
+
create policy "smoodly_read" on "assets" for select
|
|
81
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
82
|
+
|
|
83
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '7')
|
|
84
|
+
on conflict ("key") do update set "value" = excluded."value";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
-- Smoodly: read views
|
|
2
|
-
-- (2026-09-
|
|
1
|
+
-- Smoodly: read views for this app's collections, written by `npx smoodly migrate`
|
|
2
|
+
-- (2026-09-30T04:10:56.000Z). One block per collection; the marker's hash is the block's SQL, compared on the next run.
|
|
3
3
|
-- Never edit a written file: change the collection and run migrate again.
|
|
4
4
|
|
|
5
|
-
-- smoodly:collection posts sha256:
|
|
5
|
+
-- smoodly:collection posts sha256:38ba2918f83ab367
|
|
6
6
|
create or replace view "posts" with (security_invoker = true) as
|
|
7
7
|
select
|
|
8
8
|
e."id",
|
|
@@ -18,6 +18,3 @@ from "entries" e
|
|
|
18
18
|
join "entry_locales" l on l."entry_id" = e."id"
|
|
19
19
|
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
20
20
|
where e."collection" = 'posts' and l."status" = 'published';
|
|
21
|
-
|
|
22
|
-
create index if not exists "entries_posts_date_idx"
|
|
23
|
-
on "entries" (("fields"->'date')) where "collection" = 'posts';
|
|
@@ -19,7 +19,7 @@ import "../globals.css";
|
|
|
19
19
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
|
20
20
|
const { locale } = await params;
|
|
21
21
|
const site = await q().getGlobal("site", { locale });
|
|
22
|
-
return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.name ?? "Smoodly site" };
|
|
22
|
+
return { metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), title: site?.fields.name ?? "Smoodly site" };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
|
|
@@ -31,7 +31,7 @@ export default async function LocaleLayout({ children, params }: { children: Rea
|
|
|
31
31
|
<body>
|
|
32
32
|
<NextIntlClientProvider>
|
|
33
33
|
<main>{children}</main>
|
|
34
|
-
{site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
|
|
34
|
+
{site && <footer className="container site-footer">{site.fields.name} · {site.fields.tagline}</footer>}
|
|
35
35
|
</NextIntlClientProvider>
|
|
36
36
|
</body>
|
|
37
37
|
</html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { smoodly
|
|
1
|
+
import { smoodly } from "smoodly";
|
|
2
2
|
import { RichText } from "@/smoodly/richtext";
|
|
3
3
|
import { posts } from "@/smoodly/collections";
|
|
4
4
|
import { LocaleNav } from "../LocaleNav";
|
|
@@ -6,10 +6,9 @@ import { LocaleNav } from "../LocaleNav";
|
|
|
6
6
|
// The entry page for `posts`, served at /posts/<slug>. It receives the
|
|
7
7
|
// entry's fields; the body is richtext rendered by the site's bound
|
|
8
8
|
// RichText, which knows the registered elements.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const post = entry.fields as PostFields;
|
|
9
|
+
export const PostPage = smoodly.entryPage(posts, function PostView({ entry }) {
|
|
10
|
+
const { locale, locales } = entry;
|
|
11
|
+
const post = entry.fields;
|
|
13
12
|
return (
|
|
14
13
|
<>
|
|
15
14
|
<LocaleNav locale={locale} locales={locales} />
|
|
@@ -20,6 +19,5 @@ function PostView({ entry, locale, locales }: EntryPageProps) {
|
|
|
20
19
|
</article>
|
|
21
20
|
</>
|
|
22
21
|
);
|
|
23
|
-
}
|
|
22
|
+
});
|
|
24
23
|
|
|
25
|
-
export const PostPage = smoodly.entryPage(posts, PostView);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSmoodlyAdmin } from "smoodly/admin/next";
|
|
2
2
|
import { config } from "./config";
|
|
3
3
|
import { smoodlyOp } from "./ops";
|
|
4
|
-
import { publicAuth,
|
|
4
|
+
import { publicAuth, presence } from "./server";
|
|
5
5
|
|
|
6
|
-
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth,
|
|
6
|
+
export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth, presence });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// admin route enters it from config.ts — so nothing here may read the
|
|
10
10
|
// config at module scope.
|
|
11
11
|
import {
|
|
12
|
-
createSmoodlyClient, createSmoodlySite,
|
|
12
|
+
createSmoodlyClient, createSmoodlySite, presenceConfig, smoodlyEnv, SupabaseAssetStore, SupabaseEntryStore, SupabaseMemberStore, SupabasePageStore, SupabasePathIndex, type PresenceConfig, type SmoodlySite,
|
|
13
13
|
} from "smoodly";
|
|
14
14
|
// The admin gate from `smoodly/admin`, which holds no client code: the site
|
|
15
15
|
// routes import this file, and `smoodly/admin/next` would put the admin
|
|
@@ -90,8 +90,8 @@ export function publicAuth(): AuthConfig {
|
|
|
90
90
|
|
|
91
91
|
/** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
|
|
92
92
|
* The space is asked of the database once per process. */
|
|
93
|
-
export function
|
|
94
|
-
return
|
|
93
|
+
export function presence(): Promise<PresenceConfig> {
|
|
94
|
+
return presenceConfig(smoodlyEnv());
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Locale-relative CMS path → site URL: the one seam for the renderer
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- Smoodly core schema, version 7 (docs/superpowers/specs/2026-09-29-content-api-contract-design.md §5).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 6: smoodly_list_entries, which filters, orders and
|
|
5
|
+
-- limits a collection over the snapshot the reader is shown, and the read
|
|
6
|
+
-- policies of every content table admit a write key only (§8).
|
|
7
|
+
|
|
8
|
+
-- The shown snapshot of one collection's entries in a locale, filtered,
|
|
9
|
+
-- ordered and limited in one query (spec 2026-09-29 content API contract
|
|
10
|
+
-- §5): the published snapshot, or the live merge in preview. Invoker
|
|
11
|
+
-- rights, so RLS decides what a key sees; a preview read needs the
|
|
12
|
+
-- service role or a write key, anything else reads published.
|
|
13
|
+
create or replace function "smoodly_list_entries"(
|
|
14
|
+
p_collection text, p_locale text, p_draft boolean, p_where jsonb, p_order jsonb,
|
|
15
|
+
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
16
|
+
) returns table ("id" uuid, "fields" jsonb) language sql stable set search_path = public as $$
|
|
17
|
+
with mode as (
|
|
18
|
+
select p_draft and (current_user = 'service_role' or "smoodly_key_kind"() = 'write') as draft
|
|
19
|
+
), shown as (
|
|
20
|
+
select e."id", e."sort", e."created_at", l."updated_at",
|
|
21
|
+
case when (select draft from mode) then e."fields" || l."fields" || jsonb_build_object('slug', l."slug")
|
|
22
|
+
else coalesce(v."fields", e."fields" || l."fields" || jsonb_build_object('slug', l."slug")) end as "fields"
|
|
23
|
+
from "entries" e
|
|
24
|
+
join "entry_locales" l on l."entry_id" = e."id" and l."locale" = p_locale
|
|
25
|
+
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
26
|
+
where e."collection" = p_collection
|
|
27
|
+
and ((select draft from mode) or l."status" = 'published')
|
|
28
|
+
and (not p_has_parent or e."parent_id" is not distinct from p_parent)
|
|
29
|
+
and (p_ids is null or e."id" = any(p_ids))
|
|
30
|
+
)
|
|
31
|
+
select s."id", s."fields" from shown s
|
|
32
|
+
where not exists (
|
|
33
|
+
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
34
|
+
where not (case when w.value ? 'in'
|
|
35
|
+
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
36
|
+
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
37
|
+
order by
|
|
38
|
+
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
39
|
+
case when p_order ->> 'direction' = 'asc' then
|
|
40
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
41
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
42
|
+
case when p_order ->> 'direction' = 'desc' then
|
|
43
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
44
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
45
|
+
s."created_at" asc, s."id" asc
|
|
46
|
+
limit p_limit;
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
-- No public content reads (spec 2026-09-29 content API contract §8): the
|
|
50
|
+
-- publishable key and a read key read nothing; a write key reads its own space.
|
|
51
|
+
drop policy if exists "smoodly_read" on "pages";
|
|
52
|
+
create policy "smoodly_read" on "pages" for select
|
|
53
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
54
|
+
|
|
55
|
+
drop policy if exists "smoodly_read" on "page_locales";
|
|
56
|
+
create policy "smoodly_read" on "page_locales" for select
|
|
57
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
58
|
+
|
|
59
|
+
drop policy if exists "smoodly_read" on "page_versions";
|
|
60
|
+
create policy "smoodly_read" on "page_versions" for select
|
|
61
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
62
|
+
|
|
63
|
+
drop policy if exists "smoodly_read" on "entries";
|
|
64
|
+
create policy "smoodly_read" on "entries" for select
|
|
65
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
66
|
+
|
|
67
|
+
drop policy if exists "smoodly_read" on "entry_locales";
|
|
68
|
+
create policy "smoodly_read" on "entry_locales" for select
|
|
69
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
70
|
+
|
|
71
|
+
drop policy if exists "smoodly_read" on "entry_versions";
|
|
72
|
+
create policy "smoodly_read" on "entry_versions" for select
|
|
73
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
74
|
+
|
|
75
|
+
drop policy if exists "smoodly_read" on "paths";
|
|
76
|
+
create policy "smoodly_read" on "paths" for select
|
|
77
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
78
|
+
|
|
79
|
+
drop policy if exists "smoodly_read" on "assets";
|
|
80
|
+
create policy "smoodly_read" on "assets" for select
|
|
81
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
82
|
+
|
|
83
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '7')
|
|
84
|
+
on conflict ("key") do update set "value" = excluded."value";
|