create-smoodly-app 0.0.49 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +28 -10
  3. package/package.json +1 -1
  4. package/templates/astro-blank/package.json +1 -1
  5. package/templates/astro-blank-intl/package.json +1 -1
  6. package/templates/astro-lorem/package.json +1 -1
  7. package/templates/astro-lorem-intl/package.json +1 -1
  8. package/templates/blank/README.md +1 -2
  9. package/templates/blank/app/(site)/[[...path]]/page.tsx +23 -0
  10. package/templates/blank/package.json +1 -1
  11. package/templates/blank/smoodly/server.ts +11 -11
  12. package/templates/blank-intl/README.md +1 -2
  13. package/templates/blank-intl/app/[locale]/[[...path]]/page.tsx +25 -0
  14. package/templates/blank-intl/package.json +1 -1
  15. package/templates/blank-intl/smoodly/server.ts +11 -11
  16. package/templates/lorem/README.md +1 -2
  17. package/templates/lorem/app/(site)/[[...path]]/page.tsx +23 -0
  18. package/templates/lorem/package.json +1 -1
  19. package/templates/lorem/smoodly/server.ts +11 -11
  20. package/templates/lorem-intl/README.md +1 -2
  21. package/templates/lorem-intl/app/[locale]/[[...path]]/page.tsx +25 -0
  22. package/templates/lorem-intl/package.json +1 -1
  23. package/templates/lorem-intl/smoodly/server.ts +11 -11
  24. package/templates/blank/app/(site)/[...path]/page.tsx +0 -15
  25. package/templates/blank/app/(site)/page.tsx +0 -11
  26. package/templates/blank-intl/app/[locale]/[...path]/page.tsx +0 -18
  27. package/templates/blank-intl/app/[locale]/page.tsx +0 -15
  28. package/templates/lorem/app/(site)/[...path]/page.tsx +0 -15
  29. package/templates/lorem/app/(site)/page.tsx +0 -11
  30. package/templates/lorem-intl/app/[locale]/[...path]/page.tsx +0 -18
  31. package/templates/lorem-intl/app/[locale]/page.tsx +0 -15
package/README.md CHANGED
@@ -44,5 +44,8 @@ written, and a first editor who can sign in.
44
44
  Every prompt has a flag, so CI can run it: `--framework astro|next`, `--supabase local|hosted`, `--locales`,
45
45
  `--url`, `--publishable-key`, `--secret-key`, `--editor-email` (password from
46
46
  `SMOODLY_EDITOR_PASSWORD`), `--skip-link`, `--yes`. `--help` lists them.
47
+ `--cloud` takes a Smoodly Cloud space's three values as a hosted project and
48
+ skips `supabase init`, link, `db push` and the first editor: the shared
49
+ project is migrated by Smoodly Cloud and the space's owner is its admin.
47
50
  A hidden `--smoodly-dep <spec>` overrides the `smoodly` dependency written into
48
51
  `package.json`; CI points it at the packed tarball.
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ function parseFlags(argv) {
34
34
  starter: { type: "string" },
35
35
  framework: { type: "string" },
36
36
  "skip-link": { type: "boolean", default: false },
37
+ cloud: { type: "boolean", default: false },
37
38
  yes: { type: "boolean", short: "y", default: false },
38
39
  help: { type: "boolean", short: "h", default: false }
39
40
  }
@@ -43,6 +44,9 @@ function parseFlags(argv) {
43
44
  if (supabase !== void 0 && supabase !== "local" && supabase !== "hosted") {
44
45
  throw new Error(`--supabase must be "local" or "hosted", got "${supabase}"`);
45
46
  }
47
+ const cloud = values.cloud ?? false;
48
+ if (cloud && supabase === "local") throw new Error("--cloud takes a Smoodly Cloud space's values; it cannot be combined with --supabase local.");
49
+ if (cloud && values["editor-email"]) throw new Error("--cloud has no first editor to create: your smoodly.io account is the space's admin, and --editor-email does not apply. Invite others from the space page.");
46
50
  const starter = values.starter;
47
51
  if (starter !== void 0 && starter !== "lorem" && starter !== "blank") {
48
52
  throw new Error(`--starter must be "lorem" or "blank", got "${starter}"`);
@@ -53,12 +57,13 @@ function parseFlags(argv) {
53
57
  }
54
58
  return {
55
59
  name: positionals[0],
56
- supabase,
60
+ supabase: cloud ? "hosted" : supabase,
57
61
  url: values.url,
58
62
  publishableKey: values["publishable-key"],
59
63
  secretKey: values["secret-key"],
60
64
  editorEmail: values["editor-email"],
61
65
  skipLink: values["skip-link"] ?? false,
66
+ cloud,
62
67
  yes: values.yes ?? false,
63
68
  help: values.help ?? false,
64
69
  smoodlyDep: values["smoodly-dep"],
@@ -84,6 +89,7 @@ Options:
84
89
  --secret-key <key> Hosted: the service-role / secret key
85
90
  --editor-email <email> The first editor (password from SMOODLY_EDITOR_PASSWORD)
86
91
  --skip-link Hosted: do not run supabase link / db push
92
+ --cloud Smoodly Cloud: paste the three values from your space page; no supabase link or db push
87
93
  -y, --yes Accept every default
88
94
  -h, --help This text
89
95
  `;
@@ -168,12 +174,20 @@ import * as p from "@clack/prompts";
168
174
  import { resolve as resolve2 } from "node:path";
169
175
 
170
176
  // src/env.ts
171
- function renderEnv(keys, siteUrl2 = "http://localhost:3000") {
172
- return [
177
+ function renderEnv(keys, siteUrl2 = "http://localhost:3000", mode = "self-host") {
178
+ const header = mode === "cloud" ? [
179
+ "# Smoodly Cloud: the values from your space page at smoodly.io/spaces.",
180
+ "# The live site reads with SMOODLY_URL and SMOODLY_SECRET_KEY; the admin",
181
+ "# also signs in with SMOODLY_PUBLISHABLE_KEY. Replace the key from the",
182
+ "# space page if it ever leaks."
183
+ ] : [
173
184
  "# Smoodly, self-host: the content project's values. The live site reads",
174
185
  "# with SMOODLY_URL and SMOODLY_SECRET_KEY; the admin also signs in with",
175
186
  "# SMOODLY_PUBLISHABLE_KEY. SMOODLY_AUTH_URL and SMOODLY_AUTH_KEY default",
176
- "# to these \u2014 leave them unset.",
187
+ "# to these \u2014 leave them unset."
188
+ ];
189
+ return [
190
+ ...header,
177
191
  `SMOODLY_URL=${keys.url}`,
178
192
  `SMOODLY_SECRET_KEY=${keys.secretKey}`,
179
193
  `SMOODLY_PUBLISHABLE_KEY=${keys.publishableKey}`,
@@ -383,6 +397,9 @@ async function resolveAnswers(flags2, ctx, env = process.env) {
383
397
  if (!url) throw new Error(`${badUrl} (got "${typedUrl}")`);
384
398
  const publishableKey = flags2.publishableKey ?? await answer(p.text({ message: "Publishable (anon) key" }));
385
399
  const secretKey = flags2.secretKey ?? await answer(p.password({ message: "Secret (service role) key" }));
400
+ if (flags2.cloud) {
401
+ return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush: false, cloud: true }, smoodlyDep: flags2.smoodlyDep, locales, starter, framework };
402
+ }
386
403
  const ref = projectRef(url);
387
404
  if (ctx.hasSupabaseCli && !flags2.skipLink && !ref) {
388
405
  p.log.warn(`No project ref in ${url}; skipping supabase link. Run \`supabase link --project-ref <project-ref>\` and \`supabase db push\` yourself.`);
@@ -431,11 +448,12 @@ function plan(a) {
431
448
  }
432
449
  const steps = ["copyTemplate", "install"];
433
450
  if (a.supabase === "local") steps.push("supabaseLocal");
434
- else if (a.hasSupabaseCli) {
451
+ else if (a.hasSupabaseCli && !a.hosted?.cloud) {
435
452
  steps.push("supabaseInit");
436
453
  if (a.hosted?.linkAndPush) steps.push("linkAndPush");
437
454
  }
438
- steps.push("writeEnv", "ensureEditor", "seed", "gitInit", "finish");
455
+ steps.push("writeEnv", ...a.hosted?.cloud ? [] : ["ensureEditor"], "seed");
456
+ steps.push("gitInit", "finish");
439
457
  return steps;
440
458
  }
441
459
 
@@ -555,7 +573,7 @@ async function run(answers2, deps) {
555
573
  await sh("supabase", ["db", "push"]);
556
574
  break;
557
575
  case "writeEnv":
558
- await deps.fs.writeFile(join2(cwd, envFile(answers2)), renderEnv(ctx.keys, siteUrl(answers2)));
576
+ await deps.fs.writeFile(join2(cwd, envFile(answers2)), renderEnv(ctx.keys, siteUrl(answers2), answers2.hosted?.cloud ? "cloud" : "self-host"));
559
577
  break;
560
578
  case "ensureEditor":
561
579
  ctx.editor = await ensureEditor(deps.editorClient(ctx.keys), deps.askEditor);
@@ -580,7 +598,7 @@ async function run(answers2, deps) {
580
598
  return { ok: false, step, error, fallback: fallbackFor(step, answers2, ctx.keys) };
581
599
  }
582
600
  }
583
- return { ok: true, keys: ctx.keys, editor: ctx.editor };
601
+ return ctx.editor ? { ok: true, keys: ctx.keys, editor: ctx.editor } : { ok: true, keys: ctx.keys };
584
602
  }
585
603
  var withoutSeedSql = (toml) => toml.replace(/(\[db\.seed\]\n(?:#.*\n)*enabled = )true/, "$1false");
586
604
  var envFile = (answers2) => answers2.framework === "astro" ? ".env" : ".env.local";
@@ -589,7 +607,7 @@ function fallbackFor(step, answers2, keys) {
589
607
  const envLines = keys ? `Write ${answers2.dir}/${envFile(answers2)}:
590
608
 
591
609
  ${renderEnv(keys, siteUrl(answers2)).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimEnd()}` : `Write ${answers2.dir}/${envFile(answers2)} with SMOODLY_URL, SMOODLY_SECRET_KEY and SMOODLY_PUBLISHABLE_KEY (from \`supabase status\`), and SITE_URL=${siteUrl(answers2)}.`;
592
- const editorLines = [
610
+ const editorLines = answers2.hosted?.cloud ? ["Then the starter content:", "", `cd ${answers2.dir} && npm run seed`].join("\n") : [
593
611
  "Create the first editor, then the starter content:",
594
612
  "",
595
613
  `cd ${answers2.dir} && SMOODLY_ADMIN_PASSWORD=<a password> npx smoodly add-admin <your email>`,
@@ -698,7 +716,7 @@ p2.note(
698
716
  "",
699
717
  answers.starter === "blank" ? `Site: http://localhost:${port} (a blank site \u2014 build it in ${answers.framework === "astro" ? "src/sections/, src/components/" : "components/"} and at /admin)` : `Site: http://localhost:${port} (the Lorem Co. starter \u2014 edit it at /admin)`,
700
718
  `Admin: http://localhost:${port}/admin`,
701
- `Editor: ${outcome.editor.email ?? outcome.editor.id}`,
719
+ outcome.editor ? `Editor: ${outcome.editor.email ?? outcome.editor.id}` : "Editor: your smoodly.io account (invite others from the space page)",
702
720
  ...answers.locales ? [`Locales: ${answers.locales.join(", ")} (${answers.framework === "astro" ? "astro.config.ts and src/smoodly/config.ts" : "i18n/routing.ts"})`] : []
703
721
  ].join("\n"),
704
722
  "Ready"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
4
4
  "description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,7 +13,7 @@
13
13
  "@astrojs/node": "^11.1.6",
14
14
  "@supabase/supabase-js": "^2.113.0",
15
15
  "astro": "^7.3.5",
16
- "smoodly": "0.0.49"
16
+ "smoodly": "0.0.51"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@astrojs/check": "^0.9.10",
@@ -13,7 +13,7 @@
13
13
  "@astrojs/node": "^11.1.6",
14
14
  "@supabase/supabase-js": "^2.113.0",
15
15
  "astro": "^7.3.5",
16
- "smoodly": "0.0.49"
16
+ "smoodly": "0.0.51"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@astrojs/check": "^0.9.10",
@@ -13,7 +13,7 @@
13
13
  "@astrojs/node": "^11.1.6",
14
14
  "@supabase/supabase-js": "^2.113.0",
15
15
  "astro": "^7.3.5",
16
- "smoodly": "0.0.49"
16
+ "smoodly": "0.0.51"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@astrojs/check": "^0.9.10",
@@ -13,7 +13,7 @@
13
13
  "@astrojs/node": "^11.1.6",
14
14
  "@supabase/supabase-js": "^2.113.0",
15
15
  "astro": "^7.3.5",
16
- "smoodly": "0.0.49"
16
+ "smoodly": "0.0.51"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@astrojs/check": "^0.9.10",
@@ -8,8 +8,7 @@ for an agent (or a person) building the site.
8
8
 
9
9
  ## Where things are
10
10
 
11
- - `app/(site)/page.tsx` — the home page: one line calling renderSmoodlyPath([]).
12
- - `app/(site)/[...path]/page.tsx` — every other page and every collection entry, at any depth.
11
+ - `app/(site)/[[...path]]/page.tsx` — every CMS URL: the home, every page at any depth and every collection entry, rendered on first visit and cached until a publish expires it.
13
12
  - `app/(site)/layout.tsx` — the site's root layout: `app/globals.css`, the footer line from the `site` item. The admin is one route, `app/(smoodly)/admin/[[...segments]]/route.ts`, and takes no layout.
14
13
  - `components/pages/` — the page shapes and the entry page: `Home` (fixed at /, one freeform zone), `Page` (open, editors create many), `Post` (the entry page for posts).
15
14
  - `components/sections/Text.schema.ts` and `Text.tsx` — the one section: a title and a richtext body. The schema is listed in `smoodly/sections.ts`, the component in `smoodly/views.ts`.
@@ -0,0 +1,23 @@
1
+ import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
+
3
+ type Params = Promise<{ path?: string[] }>;
4
+ type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
+
6
+ // Smoodly: every CMS URL — the home (no segments), pages at any depth and
7
+ // collection entries under their `path`. A route file of your own beside
8
+ // this one, app/(site)/<segment>/page.tsx, wins for that exact path; deeper
9
+ // paths still resolve here. Each address renders on its first visit and is
10
+ // served from the cache until a publish expires it, so nothing in a page
11
+ // under this route may read cookies, headers or search params (a page that
12
+ // needs them is its own route file).
13
+ export function generateStaticParams() {
14
+ return [];
15
+ }
16
+
17
+ export async function generateMetadata({ params }: { params: Params }) {
18
+ return smoodlyMetadata((await params).path ?? []);
19
+ }
20
+
21
+ export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
22
+ return renderSmoodlyPath((await params).path ?? [], { searchParams });
23
+ }
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.49"
17
+ "smoodly": "0.0.51"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -16,8 +16,7 @@ import {
16
16
  // The admin's server pieces: the sign-in check and the content API. No browser code lives in the package's
17
17
  // entries; the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
18
18
  import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
19
- import { createPageRenderer, createSmoodlyQueries } from "smoodly/next";
20
- import { connection } from "next/server";
19
+ import { buildGate, createPageRenderer, createSmoodlyQueries } from "smoodly/next";
21
20
  import type { SupabaseClient } from "@supabase/supabase-js";
22
21
  import { collections, config, globals } from "./config";
23
22
  import { views } from "./views";
@@ -97,16 +96,17 @@ export const href = (path: string, _locale: string) => path;
97
96
  export const q = (): Queries =>
98
97
  new Proxy({} as Queries, {
99
98
  get: (_, name) => async (...args: unknown[]) => {
100
- await connection();
99
+ await buildGate();
101
100
  return (bound().q as unknown as Record<string | symbol, (...a: unknown[]) => unknown>)[name](...args);
102
101
  },
103
102
  });
104
103
 
105
- // A request first, then the env: `await connection()` makes the route dynamic before bound()
106
- // reads SMOODLY_*, so `next build` prerenders nothing here and needs no keys — a layout
107
- // reading a global through q() included (2026-10-02, tester round 6).
108
- /** The two route files call these. */
109
- export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPath(...args); };
110
- export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyMetadata(...args); };
111
- export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPage(...args); };
112
- export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyPageMetadata(...args); };
104
+ // The gate first, then the env: `buildGate()` waits for a request while `next build` prerenders, so a
105
+ // route file without a dynamic segment reads nothing at build and the build needs no keys — a layout
106
+ // reading a global through q() included (2026-10-02, tester round 6). At run time it does nothing: a
107
+ // CMS route renders on its first visit and is cached until a publish expires it (plan 2026-10-09).
108
+ /** The route file calls these. */
109
+ export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPath(...args); };
110
+ export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyMetadata(...args); };
111
+ export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPage(...args); };
112
+ export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyPageMetadata(...args); };
@@ -8,8 +8,7 @@ for an agent (or a person) building the site.
8
8
 
9
9
  ## Where things are
10
10
 
11
- - `app/[locale]/page.tsx` — the home page: one line calling renderSmoodlyPath([], { locale }).
12
- - `app/[locale]/[...path]/page.tsx` — every other page and every collection entry, at any depth, with translated segments per locale.
11
+ - `app/[locale]/[[...path]]/page.tsx` — every CMS URL: the home, every page at any depth and every collection entry, with translated segments per locale, rendered on first visit and cached until a publish expires it.
13
12
  - `app/[locale]/layout.tsx` — the site's root layout: `app/globals.css`, the footer line from the `site` item. The admin is one route, `app/(smoodly)/admin/[[...segments]]/route.ts`, and takes no layout.
14
13
  - `i18n/routing.ts` — the locales and the default; `smoodly/config.ts` reads them from here. `i18n/request.ts` and `i18n/navigation.ts` are next-intl's stock files; `proxy.ts` is its locale routing (`/admin` excluded). UI strings go in `messages/<locale>.json`.
15
14
  - `components/pages/` — the page shapes and the entry page: `Home` (fixed at /, one freeform zone), `Page` (open, editors create many), `Post` (the entry page for posts).
@@ -0,0 +1,25 @@
1
+ import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
+
3
+ type Params = Promise<{ locale: string; path?: string[] }>;
4
+ type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
+
6
+ // Smoodly: every CMS URL in this language — the home (no segments), pages
7
+ // at any depth and collection entries under their `path`. A route file of
8
+ // your own beside this one, app/[locale]/<segment>/page.tsx, wins for that
9
+ // exact path; deeper paths still resolve here. Each address renders on its
10
+ // first visit and is served from the cache until a publish expires it, so
11
+ // nothing in a page under this route may read cookies, headers or search
12
+ // params (a page that needs them is its own route file).
13
+ export function generateStaticParams() {
14
+ return [];
15
+ }
16
+
17
+ export async function generateMetadata({ params }: { params: Params }) {
18
+ const { locale, path = [] } = await params;
19
+ return smoodlyMetadata(path, { locale });
20
+ }
21
+
22
+ export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
23
+ const { locale, path = [] } = await params;
24
+ return renderSmoodlyPath(path, { locale, searchParams });
25
+ }
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.49",
17
+ "smoodly": "0.0.51",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -16,8 +16,7 @@ import {
16
16
  // The admin's server pieces: the sign-in check and the content API. No browser code lives in the package's
17
17
  // entries; the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
18
18
  import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
19
- import { createPageRenderer, createSmoodlyQueries } from "smoodly/next";
20
- import { connection } from "next/server";
19
+ import { buildGate, createPageRenderer, createSmoodlyQueries } from "smoodly/next";
21
20
  import type { SupabaseClient } from "@supabase/supabase-js";
22
21
  import { getPathname } from "@/i18n/navigation";
23
22
  import { collections, config, globals } from "./config";
@@ -98,16 +97,17 @@ export const href = (path: string, locale: string) => getPathname({ locale, href
98
97
  export const q = (): Queries =>
99
98
  new Proxy({} as Queries, {
100
99
  get: (_, name) => async (...args: unknown[]) => {
101
- await connection();
100
+ await buildGate();
102
101
  return (bound().q as unknown as Record<string | symbol, (...a: unknown[]) => unknown>)[name](...args);
103
102
  },
104
103
  });
105
104
 
106
- // A request first, then the env: `await connection()` makes the route dynamic before bound()
107
- // reads SMOODLY_*, so `next build` prerenders nothing here and needs no keys — a layout
108
- // reading a global through q() included (2026-10-02, tester round 6).
109
- /** The two route files call these. */
110
- export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPath(...args); };
111
- export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyMetadata(...args); };
112
- export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPage(...args); };
113
- export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyPageMetadata(...args); };
105
+ // The gate first, then the env: `buildGate()` waits for a request while `next build` prerenders, so a
106
+ // route file without a dynamic segment reads nothing at build and the build needs no keys — a layout
107
+ // reading a global through q() included (2026-10-02, tester round 6). At run time it does nothing: a
108
+ // CMS route renders on its first visit and is cached until a publish expires it (plan 2026-10-09).
109
+ /** The route file calls these. */
110
+ export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPath(...args); };
111
+ export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyMetadata(...args); };
112
+ export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPage(...args); };
113
+ export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyPageMetadata(...args); };
@@ -8,8 +8,7 @@ for an agent (or a person) building the site.
8
8
 
9
9
  ## Where things are
10
10
 
11
- - `app/(site)/page.tsx` — the home page: one line calling renderSmoodlyPath([]).
12
- - `app/(site)/[...path]/page.tsx` — every other page and every collection entry, at any depth.
11
+ - `app/(site)/[[...path]]/page.tsx` — every CMS URL: the home, every page at any depth and every collection entry, rendered on first visit and cached until a publish expires it.
13
12
  - `app/(site)/layout.tsx` — the site's root layout: fonts, `app/globals.css`, the footer from global content. The admin is one route, `app/(smoodly)/admin/[[...segments]]/route.ts`, and takes no layout.
14
13
  - `components/pages/` — the page shapes and the entry page: `Home` (locked hero, freeform body), `Articles` (a fixed page at the collection's path — its index — with a code-fetched grid), `Page` (open; About lives here), `Article` (the entry page), `News` and `NewsPost` (a collection at the nested address `company/news`, its index and entry page).
15
14
  - `components/PageFrame.tsx` — the header and `<main>` every page view renders (a layout cannot know which page is on screen); with more than one locale, the language links from the page's `locales` map — the same page in the other language, or its home when there is no translation.
@@ -0,0 +1,23 @@
1
+ import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
+
3
+ type Params = Promise<{ path?: string[] }>;
4
+ type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
+
6
+ // Smoodly: every CMS URL — the home (no segments), pages at any depth and
7
+ // collection entries under their `path`. A route file of your own beside
8
+ // this one, app/(site)/<segment>/page.tsx, wins for that exact path; deeper
9
+ // paths still resolve here. Each address renders on its first visit and is
10
+ // served from the cache until a publish expires it, so nothing in a page
11
+ // under this route may read cookies, headers or search params (a page that
12
+ // needs them is its own route file).
13
+ export function generateStaticParams() {
14
+ return [];
15
+ }
16
+
17
+ export async function generateMetadata({ params }: { params: Params }) {
18
+ return smoodlyMetadata((await params).path ?? []);
19
+ }
20
+
21
+ export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
22
+ return renderSmoodlyPath((await params).path ?? [], { searchParams });
23
+ }
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.49"
17
+ "smoodly": "0.0.51"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -16,8 +16,7 @@ import {
16
16
  // The admin's server pieces: the sign-in check and the content API. No browser code lives in the package's
17
17
  // entries; the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
18
18
  import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
19
- import { createPageRenderer, createSmoodlyQueries } from "smoodly/next";
20
- import { connection } from "next/server";
19
+ import { buildGate, createPageRenderer, createSmoodlyQueries } from "smoodly/next";
21
20
  import type { SupabaseClient } from "@supabase/supabase-js";
22
21
  import { collections, config, globals } from "./config";
23
22
  import { views } from "./views";
@@ -97,16 +96,17 @@ export const href = (path: string, _locale: string) => path;
97
96
  export const q = (): Queries =>
98
97
  new Proxy({} as Queries, {
99
98
  get: (_, name) => async (...args: unknown[]) => {
100
- await connection();
99
+ await buildGate();
101
100
  return (bound().q as unknown as Record<string | symbol, (...a: unknown[]) => unknown>)[name](...args);
102
101
  },
103
102
  });
104
103
 
105
- // A request first, then the env: `await connection()` makes the route dynamic before bound()
106
- // reads SMOODLY_*, so `next build` prerenders nothing here and needs no keys — a layout
107
- // reading a global through q() included (2026-10-02, tester round 6).
108
- /** The two route files call these. */
109
- export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPath(...args); };
110
- export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyMetadata(...args); };
111
- export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPage(...args); };
112
- export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyPageMetadata(...args); };
104
+ // The gate first, then the env: `buildGate()` waits for a request while `next build` prerenders, so a
105
+ // route file without a dynamic segment reads nothing at build and the build needs no keys — a layout
106
+ // reading a global through q() included (2026-10-02, tester round 6). At run time it does nothing: a
107
+ // CMS route renders on its first visit and is cached until a publish expires it (plan 2026-10-09).
108
+ /** The route file calls these. */
109
+ export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPath(...args); };
110
+ export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyMetadata(...args); };
111
+ export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPage(...args); };
112
+ export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyPageMetadata(...args); };
@@ -8,8 +8,7 @@ for an agent (or a person) building the site.
8
8
 
9
9
  ## Where things are
10
10
 
11
- - `app/[locale]/page.tsx` — the home page: one line calling renderSmoodlyPath([], { locale }).
12
- - `app/[locale]/[...path]/page.tsx` — every other page and every collection entry, at any depth, with translated segments per locale.
11
+ - `app/[locale]/[[...path]]/page.tsx` — every CMS URL: the home, every page at any depth and every collection entry, with translated segments per locale, rendered on first visit and cached until a publish expires it.
13
12
  - `app/[locale]/layout.tsx` — the site's root layout: fonts, `app/globals.css`, the footer from global content. The admin is one route, `app/(smoodly)/admin/[[...segments]]/route.ts`, and takes no layout.
14
13
  - `i18n/routing.ts` — the locales and the default; `smoodly/config.ts` reads them from here. `i18n/request.ts` and `i18n/navigation.ts` are next-intl's stock files; `proxy.ts` is its locale routing (`/admin` excluded).
15
14
  - `components/pages/` — the page shapes and the entry page: `Home` (locked hero, freeform body), `Articles` (a fixed page at the collection's path — its index — with a code-fetched grid), `Page` (open; About lives here), `Article` (the entry page), `News` and `NewsPost` (a collection at the nested address `company/news`, its index and entry page).
@@ -0,0 +1,25 @@
1
+ import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
+
3
+ type Params = Promise<{ locale: string; path?: string[] }>;
4
+ type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
+
6
+ // Smoodly: every CMS URL in this language — the home (no segments), pages
7
+ // at any depth and collection entries under their `path`. A route file of
8
+ // your own beside this one, app/[locale]/<segment>/page.tsx, wins for that
9
+ // exact path; deeper paths still resolve here. Each address renders on its
10
+ // first visit and is served from the cache until a publish expires it, so
11
+ // nothing in a page under this route may read cookies, headers or search
12
+ // params (a page that needs them is its own route file).
13
+ export function generateStaticParams() {
14
+ return [];
15
+ }
16
+
17
+ export async function generateMetadata({ params }: { params: Params }) {
18
+ const { locale, path = [] } = await params;
19
+ return smoodlyMetadata(path, { locale });
20
+ }
21
+
22
+ export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
23
+ const { locale, path = [] } = await params;
24
+ return renderSmoodlyPath(path, { locale, searchParams });
25
+ }
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.49",
17
+ "smoodly": "0.0.51",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -16,8 +16,7 @@ import {
16
16
  // The admin's server pieces: the sign-in check and the content API. No browser code lives in the package's
17
17
  // entries; the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
18
18
  import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
19
- import { createPageRenderer, createSmoodlyQueries } from "smoodly/next";
20
- import { connection } from "next/server";
19
+ import { buildGate, createPageRenderer, createSmoodlyQueries } from "smoodly/next";
21
20
  import type { SupabaseClient } from "@supabase/supabase-js";
22
21
  import { getPathname } from "@/i18n/navigation";
23
22
  import { collections, config, globals } from "./config";
@@ -98,16 +97,17 @@ export const href = (path: string, locale: string) => getPathname({ locale, href
98
97
  export const q = (): Queries =>
99
98
  new Proxy({} as Queries, {
100
99
  get: (_, name) => async (...args: unknown[]) => {
101
- await connection();
100
+ await buildGate();
102
101
  return (bound().q as unknown as Record<string | symbol, (...a: unknown[]) => unknown>)[name](...args);
103
102
  },
104
103
  });
105
104
 
106
- // A request first, then the env: `await connection()` makes the route dynamic before bound()
107
- // reads SMOODLY_*, so `next build` prerenders nothing here and needs no keys — a layout
108
- // reading a global through q() included (2026-10-02, tester round 6).
109
- /** The two route files call these. */
110
- export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPath(...args); };
111
- export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyMetadata(...args); };
112
- export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await connection(); return bound().renderer.renderSmoodlyPage(...args); };
113
- export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await connection(); return bound().renderer.smoodlyPageMetadata(...args); };
105
+ // The gate first, then the env: `buildGate()` waits for a request while `next build` prerenders, so a
106
+ // route file without a dynamic segment reads nothing at build and the build needs no keys — a layout
107
+ // reading a global through q() included (2026-10-02, tester round 6). At run time it does nothing: a
108
+ // CMS route renders on its first visit and is cached until a publish expires it (plan 2026-10-09).
109
+ /** The route file calls these. */
110
+ export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPath(...args); };
111
+ export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyMetadata(...args); };
112
+ export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPage(...args); };
113
+ export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyPageMetadata(...args); };
@@ -1,15 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ path: string[] }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: everything else — pages at any depth and collection entries
7
- // under their `path`. Add app/(site)/<segment>/page.tsx beside this file
8
- // and it wins for that exact path; deeper paths still resolve here.
9
- export async function generateMetadata({ params }: { params: Params }) {
10
- return smoodlyMetadata((await params).path);
11
- }
12
-
13
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
14
- return renderSmoodlyPath((await params).path, { searchParams });
15
- }
@@ -1,11 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
4
-
5
- // Smoodly: the home page. Editing this file replaces or wraps the CMS
6
- // home; the shapes it can render live in components/pages/.
7
- export const generateMetadata = () => smoodlyMetadata([]);
8
-
9
- export default async function Page({ searchParams }: { searchParams: SearchParams }) {
10
- return renderSmoodlyPath([], { searchParams });
11
- }
@@ -1,18 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ locale: string; path: string[] }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: everything else — pages at any depth and collection entries
7
- // under their `path`, with translated segments per locale. Add
8
- // app/[locale]/<segment>/page.tsx beside this file and it wins for that
9
- // exact path; deeper paths still resolve here.
10
- export async function generateMetadata({ params }: { params: Params }) {
11
- const { locale, path } = await params;
12
- return smoodlyMetadata(path, { locale });
13
- }
14
-
15
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
16
- const { locale, path } = await params;
17
- return renderSmoodlyPath(path, { locale, searchParams });
18
- }
@@ -1,15 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ locale: string }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: the home page, in the locale next-intl's proxy resolved. Editing
7
- // this file replaces or wraps the CMS home; the shapes it can render live
8
- // in components/pages/.
9
- export async function generateMetadata({ params }: { params: Params }) {
10
- return smoodlyMetadata([], { locale: (await params).locale });
11
- }
12
-
13
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
14
- return renderSmoodlyPath([], { locale: (await params).locale, searchParams });
15
- }
@@ -1,15 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ path: string[] }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: everything else — pages at any depth and collection entries
7
- // under their `path`. Add app/(site)/<segment>/page.tsx beside this file
8
- // and it wins for that exact path; deeper paths still resolve here.
9
- export async function generateMetadata({ params }: { params: Params }) {
10
- return smoodlyMetadata((await params).path);
11
- }
12
-
13
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
14
- return renderSmoodlyPath((await params).path, { searchParams });
15
- }
@@ -1,11 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
4
-
5
- // Smoodly: the home page. Editing this file replaces or wraps the CMS
6
- // home; the shapes it can render live in components/pages/.
7
- export const generateMetadata = () => smoodlyMetadata([]);
8
-
9
- export default async function Page({ searchParams }: { searchParams: SearchParams }) {
10
- return renderSmoodlyPath([], { searchParams });
11
- }
@@ -1,18 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ locale: string; path: string[] }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: everything else — pages at any depth and collection entries
7
- // under their `path`, with translated segments per locale. Add
8
- // app/[locale]/<segment>/page.tsx beside this file and it wins for that
9
- // exact path; deeper paths still resolve here.
10
- export async function generateMetadata({ params }: { params: Params }) {
11
- const { locale, path } = await params;
12
- return smoodlyMetadata(path, { locale });
13
- }
14
-
15
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
16
- const { locale, path } = await params;
17
- return renderSmoodlyPath(path, { locale, searchParams });
18
- }
@@ -1,15 +0,0 @@
1
- import { renderSmoodlyPath, smoodlyMetadata } from "@/smoodly/server";
2
-
3
- type Params = Promise<{ locale: string }>;
4
- type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
-
6
- // Smoodly: the home page, in the locale next-intl's proxy resolved. Editing
7
- // this file replaces or wraps the CMS home; the shapes it can render live
8
- // in components/pages/.
9
- export async function generateMetadata({ params }: { params: Params }) {
10
- return smoodlyMetadata([], { locale: (await params).locale });
11
- }
12
-
13
- export default async function Page({ params, searchParams }: { params: Params; searchParams: SearchParams }) {
14
- return renderSmoodlyPath([], { locale: (await params).locale, searchParams });
15
- }