create-smoodly-app 0.0.14 → 0.0.15

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 (173) hide show
  1. package/README.md +38 -10
  2. package/dist/index.js +39 -15
  3. package/package.json +3 -2
  4. package/templates/blank/AGENTS.md +50 -0
  5. package/templates/blank/CLAUDE.md +1 -0
  6. package/templates/blank/README.md +75 -0
  7. package/templates/{next/app → blank/app/(site)}/[...path]/page.tsx +2 -2
  8. package/templates/blank/app/(site)/layout.tsx +25 -0
  9. package/templates/blank/app/globals.css +47 -0
  10. package/templates/blank/components/elements/Button.tsx +26 -0
  11. package/templates/blank/components/pages/Home.tsx +22 -0
  12. package/templates/blank/components/pages/Page.tsx +25 -0
  13. package/templates/blank/components/pages/Post.tsx +21 -0
  14. package/templates/blank/components/sections/Text.tsx +39 -0
  15. package/templates/{next → blank}/package.json +1 -1
  16. package/templates/blank/scripts/seed.ts +72 -0
  17. package/templates/blank/smoodly/collections.ts +26 -0
  18. package/templates/blank/smoodly/config.ts +35 -0
  19. package/templates/blank/smoodly/elements.ts +5 -0
  20. package/templates/blank/smoodly/sections.ts +5 -0
  21. package/templates/blank/smoodly/shared.ts +16 -0
  22. package/templates/blank/smoodly/toolsets.ts +14 -0
  23. package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +518 -0
  24. package/templates/blank-intl/AGENTS.md +50 -0
  25. package/templates/blank-intl/CLAUDE.md +1 -0
  26. package/templates/blank-intl/README.md +76 -0
  27. package/templates/blank-intl/app/[locale]/layout.tsx +42 -0
  28. package/templates/blank-intl/app/globals.css +47 -0
  29. package/templates/blank-intl/components/elements/Button.tsx +26 -0
  30. package/templates/blank-intl/components/pages/Home.tsx +22 -0
  31. package/templates/blank-intl/components/pages/Page.tsx +25 -0
  32. package/templates/blank-intl/components/pages/Post.tsx +21 -0
  33. package/templates/blank-intl/components/sections/Text.tsx +39 -0
  34. package/templates/blank-intl/messages/en.json +1 -0
  35. package/templates/{next-intl → blank-intl}/package.json +1 -1
  36. package/templates/blank-intl/scripts/seed.ts +72 -0
  37. package/templates/blank-intl/smoodly/collections.ts +26 -0
  38. package/templates/blank-intl/smoodly/config.ts +38 -0
  39. package/templates/blank-intl/smoodly/elements.ts +5 -0
  40. package/templates/blank-intl/smoodly/sections.ts +5 -0
  41. package/templates/blank-intl/smoodly/shared.ts +16 -0
  42. package/templates/blank-intl/smoodly/toolsets.ts +14 -0
  43. package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +518 -0
  44. package/templates/blank-intl.tsconfig.json +28 -0
  45. package/templates/blank.tsconfig.json +28 -0
  46. package/templates/lorem/AGENTS.md +50 -0
  47. package/templates/lorem/CLAUDE.md +1 -0
  48. package/templates/{next → lorem}/README.md +5 -4
  49. package/templates/lorem/_gitignore +5 -0
  50. package/templates/lorem/app/(site)/[...path]/page.tsx +15 -0
  51. package/templates/{next/app → lorem/app/(site)}/layout.tsx +7 -6
  52. package/templates/lorem/app/(site)/page.tsx +11 -0
  53. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  54. package/templates/lorem/app/(smoodly)/admin/layout.tsx +1 -0
  55. package/templates/lorem/next.config.ts +8 -0
  56. package/templates/lorem/package.json +27 -0
  57. package/templates/lorem/smoodly/admin.tsx +6 -0
  58. package/templates/lorem/smoodly/ops.ts +11 -0
  59. package/templates/lorem/smoodly/richtext.tsx +7 -0
  60. package/templates/lorem/smoodly/server.ts +96 -0
  61. package/templates/lorem/tsconfig.json +41 -0
  62. package/templates/lorem-intl/AGENTS.md +50 -0
  63. package/templates/lorem-intl/CLAUDE.md +1 -0
  64. package/templates/{next-intl → lorem-intl}/README.md +2 -1
  65. package/templates/lorem-intl/_gitignore +5 -0
  66. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  67. package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +15 -0
  68. package/templates/lorem-intl/app/[locale]/[...path]/page.tsx +18 -0
  69. package/templates/lorem-intl/app/[locale]/page.tsx +15 -0
  70. package/templates/lorem-intl/i18n/navigation.ts +6 -0
  71. package/templates/lorem-intl/i18n/request.ts +24 -0
  72. package/templates/lorem-intl/i18n/routing.ts +17 -0
  73. package/templates/lorem-intl/next.config.ts +11 -0
  74. package/templates/lorem-intl/package.json +28 -0
  75. package/templates/lorem-intl/proxy.ts +11 -0
  76. package/templates/lorem-intl/smoodly/admin.tsx +6 -0
  77. package/templates/lorem-intl/smoodly/ops.ts +11 -0
  78. package/templates/lorem-intl/smoodly/richtext.tsx +7 -0
  79. package/templates/lorem-intl/smoodly/server.ts +97 -0
  80. package/templates/lorem-intl/tsconfig.json +41 -0
  81. package/templates/lorem-intl.tsconfig.json +28 -0
  82. package/templates/lorem.tsconfig.json +28 -0
  83. /package/templates/{next → blank}/_gitignore +0 -0
  84. /package/templates/{next/app → blank/app/(site)}/page.tsx +0 -0
  85. /package/templates/{next → blank}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  86. /package/templates/{next → blank}/app/(smoodly)/admin/layout.tsx +0 -0
  87. /package/templates/{next → blank}/next.config.ts +0 -0
  88. /package/templates/{next → blank}/smoodly/admin.tsx +0 -0
  89. /package/templates/{next → blank}/smoodly/ops.ts +0 -0
  90. /package/templates/{next → blank}/smoodly/richtext.tsx +0 -0
  91. /package/templates/{next → blank}/smoodly/server.ts +0 -0
  92. /package/templates/{next → blank}/tsconfig.json +0 -0
  93. /package/templates/{next-intl → blank-intl}/_gitignore +0 -0
  94. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  95. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/layout.tsx +0 -0
  96. /package/templates/{next-intl → blank-intl}/app/[locale]/[...path]/page.tsx +0 -0
  97. /package/templates/{next-intl → blank-intl}/app/[locale]/page.tsx +0 -0
  98. /package/templates/{next-intl → blank-intl}/i18n/navigation.ts +0 -0
  99. /package/templates/{next-intl → blank-intl}/i18n/request.ts +0 -0
  100. /package/templates/{next-intl → blank-intl}/i18n/routing.ts +0 -0
  101. /package/templates/{next-intl → blank-intl}/next.config.ts +0 -0
  102. /package/templates/{next-intl → blank-intl}/proxy.ts +0 -0
  103. /package/templates/{next-intl → blank-intl}/smoodly/admin.tsx +0 -0
  104. /package/templates/{next-intl → blank-intl}/smoodly/ops.ts +0 -0
  105. /package/templates/{next-intl → blank-intl}/smoodly/richtext.tsx +0 -0
  106. /package/templates/{next-intl → blank-intl}/smoodly/server.ts +0 -0
  107. /package/templates/{next-intl → blank-intl}/tsconfig.json +0 -0
  108. /package/templates/{next → lorem}/app/globals.css +0 -0
  109. /package/templates/{next → lorem}/components/ArticleCard.tsx +0 -0
  110. /package/templates/{next → lorem}/components/articles.ts +0 -0
  111. /package/templates/{next → lorem}/components/elements/Callout.tsx +0 -0
  112. /package/templates/{next → lorem}/components/elements/Cta.tsx +0 -0
  113. /package/templates/{next → lorem}/components/pages/Article.tsx +0 -0
  114. /package/templates/{next → lorem}/components/pages/Articles.tsx +0 -0
  115. /package/templates/{next → lorem}/components/pages/Home.tsx +0 -0
  116. /package/templates/{next → lorem}/components/pages/Page.tsx +0 -0
  117. /package/templates/{next → lorem}/components/sections/FavouriteArticles.tsx +0 -0
  118. /package/templates/{next → lorem}/components/sections/Footer.tsx +0 -0
  119. /package/templates/{next → lorem}/components/sections/Header.tsx +0 -0
  120. /package/templates/{next → lorem}/components/sections/Hero.tsx +0 -0
  121. /package/templates/{next → lorem}/components/sections/ImageText.tsx +0 -0
  122. /package/templates/{next → lorem}/components/sections/PageHero.tsx +0 -0
  123. /package/templates/{next → lorem}/components/sections/People.tsx +0 -0
  124. /package/templates/{next → lorem}/components/sections/Points.tsx +0 -0
  125. /package/templates/{next → lorem}/components/sections/Quote.tsx +0 -0
  126. /package/templates/{next → lorem}/components/strings.ts +0 -0
  127. /package/templates/{next → lorem}/messages/en.json +0 -0
  128. /package/templates/{next → lorem}/public/images/about_main.jpg +0 -0
  129. /package/templates/{next → lorem}/public/images/article_photo.jpg +0 -0
  130. /package/templates/{next → lorem}/public/images/hero_image.jpg +0 -0
  131. /package/templates/{next → lorem}/public/images/home_section_2.jpg +0 -0
  132. /package/templates/{next → lorem}/scripts/seed.ts +0 -0
  133. /package/templates/{next → lorem}/smoodly/collections.ts +0 -0
  134. /package/templates/{next → lorem}/smoodly/config.ts +0 -0
  135. /package/templates/{next → lorem}/smoodly/elements.ts +0 -0
  136. /package/templates/{next → lorem}/smoodly/sections.ts +0 -0
  137. /package/templates/{next → lorem}/smoodly/shared.ts +0 -0
  138. /package/templates/{next → lorem}/smoodly/toolsets.ts +0 -0
  139. /package/templates/{next → lorem}/supabase/migrations/00000000000000_smoodly.sql +0 -0
  140. /package/templates/{next-intl → lorem-intl}/app/[locale]/layout.tsx +0 -0
  141. /package/templates/{next-intl → lorem-intl}/app/globals.css +0 -0
  142. /package/templates/{next-intl → lorem-intl}/components/ArticleCard.tsx +0 -0
  143. /package/templates/{next-intl → lorem-intl}/components/articles.ts +0 -0
  144. /package/templates/{next-intl → lorem-intl}/components/elements/Callout.tsx +0 -0
  145. /package/templates/{next-intl → lorem-intl}/components/elements/Cta.tsx +0 -0
  146. /package/templates/{next-intl → lorem-intl}/components/pages/Article.tsx +0 -0
  147. /package/templates/{next-intl → lorem-intl}/components/pages/Articles.tsx +0 -0
  148. /package/templates/{next-intl → lorem-intl}/components/pages/Home.tsx +0 -0
  149. /package/templates/{next-intl → lorem-intl}/components/pages/Page.tsx +0 -0
  150. /package/templates/{next-intl → lorem-intl}/components/sections/FavouriteArticles.tsx +0 -0
  151. /package/templates/{next-intl → lorem-intl}/components/sections/Footer.tsx +0 -0
  152. /package/templates/{next-intl → lorem-intl}/components/sections/Header.tsx +0 -0
  153. /package/templates/{next-intl → lorem-intl}/components/sections/Hero.tsx +0 -0
  154. /package/templates/{next-intl → lorem-intl}/components/sections/ImageText.tsx +0 -0
  155. /package/templates/{next-intl → lorem-intl}/components/sections/PageHero.tsx +0 -0
  156. /package/templates/{next-intl → lorem-intl}/components/sections/People.tsx +0 -0
  157. /package/templates/{next-intl → lorem-intl}/components/sections/Points.tsx +0 -0
  158. /package/templates/{next-intl → lorem-intl}/components/sections/Quote.tsx +0 -0
  159. /package/templates/{next-intl → lorem-intl}/components/strings.ts +0 -0
  160. /package/templates/{next-intl → lorem-intl}/messages/en.json +0 -0
  161. /package/templates/{next-intl → lorem-intl}/messages/fi.json +0 -0
  162. /package/templates/{next-intl → lorem-intl}/public/images/about_main.jpg +0 -0
  163. /package/templates/{next-intl → lorem-intl}/public/images/article_photo.jpg +0 -0
  164. /package/templates/{next-intl → lorem-intl}/public/images/hero_image.jpg +0 -0
  165. /package/templates/{next-intl → lorem-intl}/public/images/home_section_2.jpg +0 -0
  166. /package/templates/{next-intl → lorem-intl}/scripts/seed.ts +0 -0
  167. /package/templates/{next-intl → lorem-intl}/smoodly/collections.ts +0 -0
  168. /package/templates/{next-intl → lorem-intl}/smoodly/config.ts +0 -0
  169. /package/templates/{next-intl → lorem-intl}/smoodly/elements.ts +0 -0
  170. /package/templates/{next-intl → lorem-intl}/smoodly/sections.ts +0 -0
  171. /package/templates/{next-intl → lorem-intl}/smoodly/shared.ts +0 -0
  172. /package/templates/{next-intl → lorem-intl}/smoodly/toolsets.ts +0 -0
  173. /package/templates/{next-intl → lorem-intl}/supabase/migrations/00000000000000_smoodly.sql +0 -0
package/README.md CHANGED
@@ -43,15 +43,23 @@ A hidden `--smoodly-dep <spec>` overrides the `smoodly` dependency written into
43
43
 
44
44
  ## How it is built
45
45
 
46
- `templates/next/` and `templates/next-intl/` are both generated by
47
- `scripts/build-template.ts` from `examples/site` — the multilingual layout —
48
- plus a generated `package.json` (with `smoodly` pinned to this package's
49
- version), `tsconfig`, `next.config`, README and the one migration
50
- (`coreTablesSQL` + the starter collections' views). The four single-language
51
- route files live in `variants/single/`, and two exact-line transforms turn
52
- the example's glue (locale config, `href`) into the single-language one; a
53
- drifted example fails the build rather than silently shipping stale copies.
54
- Both templates are git-ignored and rebuilt by `npm run build`.
46
+ `templates/lorem`, `templates/lorem-intl`, `templates/blank` and
47
+ `templates/blank-intl` are all generated by `scripts/build-template.ts`.
48
+ The Lorem Co. pair comes from `examples/site` — the multilingual layout —
49
+ plus the five single-language files under `variants/lorem/single/`. The
50
+ blank pair (spec 2026-09-11) takes only the content-free glue from the
51
+ example (`smoodly/server.ts`, `ops.ts`, `admin.tsx`, `richtext.tsx`, the
52
+ admin route files, next-intl's files) and everything with content from
53
+ `variants/blank/{common,single,intl}/`. Each template also gets a generated
54
+ `package.json` (with `smoodly` pinned to this package's version),
55
+ `tsconfig`, `next.config`, README, `AGENTS.md`, a `CLAUDE.md` that imports
56
+ it, and the one migration (`coreTablesSQL` + the starter's collection
57
+ views). Two exact-line transforms turn the example's glue (locale config,
58
+ `href`) into the single-language one; a drifted example fails the build
59
+ rather than silently shipping stale copies. Every template gets a check
60
+ tsconfig beside it; `npm run typecheck` runs `tsc` over all four
61
+ (`typecheck:templates`), so hand-kept files cannot drift silently. All
62
+ four templates are git-ignored and rebuilt by `npm run build`.
55
63
 
56
64
  `src/plan.ts` turns answers into steps; `src/run.ts` executes them with
57
65
  injected `exec`, fs and Supabase client so the sequence and every fallback
@@ -59,12 +67,32 @@ message are unit-tested. `src/prompts.ts` is the only interactive code.
59
67
  The CLI has no runtime dependency on `smoodly`.
60
68
 
61
69
  npm test # unit tests (template build, planner, runner, editor)
62
- npm run build # template + esbuild bundle → dist/index.js
70
+ npm run typecheck # tsc, then the four templates
71
+ npm run build # templates + esbuild bundle → dist/index.js
63
72
 
64
73
  Walked 2026-09-06: both templates scaffolded against the repo's local stack
65
74
  in hosted mode, `tsc`, `next build` and `/admin` 200 for each; the
66
75
  multilingual one with `--locales en,de`.
67
76
 
77
+ Walked 2026-09-11: blank multilingual scaffolded in local mode with
78
+ `--locales en,fr` from the built CLI, `next build`, `/` and `/fr` seeded
79
+ with the Text section and the site footer line, the admin listing Home,
80
+ Posts and the Site item, a post with a Button element published and
81
+ served at `/posts/<slug>` in both locales; blank single-language
82
+ scaffolded in hosted mode against the repo stack and `next build` only.
83
+
84
+ ## Follow-ups (logged 2026-09-11)
85
+
86
+ - **Blank single-language is typechecked, never built by CI.** The live
87
+ job scaffolds lorem single (hosted), lorem intl and blank intl (local).
88
+ A fourth stack costs ~2 minutes; add it if blank single ever breaks in
89
+ a way `tsc` cannot see (a runtime import, a missing file at a path
90
+ Next reads).
91
+ - **The guide inside the package.** `AGENTS.md` is generated per scaffold
92
+ and frozen at install time. Once its text settles, ship it in the
93
+ `smoodly` package (`smoodly/AGENTS.md`) and have the scaffold's copy
94
+ point there, so it tracks the package version.
95
+
68
96
  ## Follow-ups (logged 2026-09-06)
69
97
 
70
98
  - Local mode on a fresh multilingual install (0.0.5, 2026-09-06) failed at
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ function parseFlags(argv) {
29
29
  "editor-email": { type: "string" },
30
30
  "smoodly-dep": { type: "string" },
31
31
  locales: { type: "string" },
32
+ starter: { type: "string" },
32
33
  "skip-link": { type: "boolean", default: false },
33
34
  yes: { type: "boolean", short: "y", default: false },
34
35
  help: { type: "boolean", short: "h", default: false }
@@ -39,6 +40,10 @@ function parseFlags(argv) {
39
40
  if (supabase !== void 0 && supabase !== "local" && supabase !== "hosted") {
40
41
  throw new Error(`--supabase must be "local" or "hosted", got "${supabase}"`);
41
42
  }
43
+ const starter = values.starter;
44
+ if (starter !== void 0 && starter !== "lorem" && starter !== "blank") {
45
+ throw new Error(`--starter must be "lorem" or "blank", got "${starter}"`);
46
+ }
42
47
  return {
43
48
  name: positionals[0],
44
49
  supabase,
@@ -50,7 +55,8 @@ function parseFlags(argv) {
50
55
  yes: values.yes ?? false,
51
56
  help: values.help ?? false,
52
57
  smoodlyDep: values["smoodly-dep"],
53
- locales: values.locales === void 0 ? void 0 : parseLocales(values.locales)
58
+ locales: values.locales === void 0 ? void 0 : parseLocales(values.locales),
59
+ starter
54
60
  };
55
61
  }
56
62
  var HELP = `create-smoodly-app \u2014 a fresh Next.js site with Smoodly installed
@@ -60,6 +66,7 @@ Usage: npx create-smoodly-app [name] [options]
60
66
  Options:
61
67
  --supabase local|hosted Where content lives (asked when omitted; local needs the Supabase CLI)
62
68
  --locales <a,b,\u2026> Multilingual: the locales, first is the default (scaffolds next-intl; omitted = one language)
69
+ --starter lorem|blank lorem: the Lorem Co. tour of every feature (default); blank: one of each, for building your own
63
70
  --url <url> Hosted: the Supabase project URL
64
71
  --publishable-key <key> Hosted: the anon / publishable key
65
72
  --secret-key <key> Hosted: the service-role / secret key
@@ -227,13 +234,15 @@ async function scaffold(opts) {
227
234
  routingPath,
228
235
  routing.replace(ROUTING_LOCALES, ` locales: [${opts.locales.map((l) => JSON.stringify(l)).join(", ")}],`).replace(ROUTING_DEFAULT, ` defaultLocale: ${JSON.stringify(opts.locales[0])},`)
229
236
  );
230
- const map = articlesSegments(opts.locales);
231
- for (const [rel, line] of [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]]) {
232
- const path = join(opts.dir, rel);
233
- const source = await readFile(path, "utf8");
234
- if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
237
+ if (opts.starter !== "blank") {
238
+ const map = articlesSegments(opts.locales);
239
+ for (const [rel, line] of [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]]) {
240
+ const path = join(opts.dir, rel);
241
+ const source = await readFile(path, "utf8");
242
+ if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
235
243
  ${line}`);
236
- await writeFile(path, source.replace(line, line.replace('{ en: "posts", fi: "artikkelit" }', map)));
244
+ await writeFile(path, source.replace(line, line.replace('{ en: "posts", fi: "artikkelit" }', map)));
245
+ }
237
246
  }
238
247
  }
239
248
  }
@@ -262,7 +271,7 @@ async function resolveAnswers(flags2, ctx) {
262
271
  const locales = flags2.locales ?? (flags2.yes ? void 0 : await answer(p.confirm({ message: "Multilingual site? (next-intl, translated URLs)", initialValue: false })) ? await answer(
263
272
  p.text({
264
273
  message: "Locales, comma-separated, the first is the default",
265
- placeholder: "en, fi",
274
+ placeholder: "en, fr",
266
275
  validate: (v) => {
267
276
  try {
268
277
  parseLocales(v ?? "");
@@ -272,6 +281,15 @@ async function resolveAnswers(flags2, ctx) {
272
281
  }
273
282
  })
274
283
  ).then(parseLocales) : void 0);
284
+ const starter = flags2.starter ?? (flags2.yes ? "lorem" : await answer(
285
+ p.select({
286
+ message: "Starter?",
287
+ options: [
288
+ { value: "lorem", label: "Lorem Co.", hint: "a tour of every feature \u2014 edit it into your site" },
289
+ { value: "blank", label: "Blank", hint: "one of each \u2014 build your own" }
290
+ ]
291
+ })
292
+ ));
275
293
  const supabase = flags2.supabase ?? (flags2.yes ? "local" : await answer(
276
294
  p.select({
277
295
  message: "Where should content live?",
@@ -283,7 +301,7 @@ async function resolveAnswers(flags2, ctx) {
283
301
  ));
284
302
  if (supabase === "local") {
285
303
  if (!ctx.hasSupabaseCli) bail("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
286
- return { name, dir, supabase, hasSupabaseCli: true, smoodlyDep: flags2.smoodlyDep, locales };
304
+ return { name, dir, supabase, hasSupabaseCli: true, smoodlyDep: flags2.smoodlyDep, locales, starter };
287
305
  }
288
306
  const badUrl = "Supabase project URL must be an http(s) URL, like https://abcd.supabase.co";
289
307
  const typedUrl = flags2.url ?? await answer(
@@ -302,7 +320,7 @@ async function resolveAnswers(flags2, ctx) {
302
320
  p.log.warn(`No project ref in ${url}; skipping supabase link. Run \`supabase link --project-ref <project-ref>\` and \`supabase db push\` yourself.`);
303
321
  }
304
322
  const linkAndPush = !ctx.hasSupabaseCli ? false : flags2.skipLink || !ref ? false : flags2.yes ? true : await answer(p.confirm({ message: `Link this app to the project and push the migration now? (supabase link --project-ref ${ref}, supabase db push)` }));
305
- return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush }, smoodlyDep: flags2.smoodlyDep, locales };
323
+ return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush }, smoodlyDep: flags2.smoodlyDep, locales, starter };
306
324
  }
307
325
  function askEditorWith(flags2, env = process.env) {
308
326
  return async (existing) => {
@@ -335,6 +353,9 @@ function askEditorWith(flags2, env = process.env) {
335
353
  import { join as join2 } from "node:path";
336
354
 
337
355
  // src/plan.ts
356
+ function templateName(starter, locales) {
357
+ return locales === "intl" ? `${starter}-intl` : starter;
358
+ }
338
359
  function plan(a) {
339
360
  if (a.supabase === "local" && !a.hasSupabaseCli) {
340
361
  throw new Error("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
@@ -409,11 +430,12 @@ async function run(answers2, deps) {
409
430
  switch (step) {
410
431
  case "copyTemplate":
411
432
  await deps.fs.scaffold({
412
- templateDir: answers2.locales ? deps.templateDirs.intl : deps.templateDirs.single,
433
+ templateDir: deps.templateDirs[templateName(answers2.starter ?? "lorem", answers2.locales ? "intl" : "single")],
413
434
  dir: cwd,
414
435
  name: answers2.name,
415
436
  smoodlyDep: answers2.smoodlyDep,
416
- locales: answers2.locales
437
+ locales: answers2.locales,
438
+ starter: answers2.starter
417
439
  });
418
440
  break;
419
441
  case "install":
@@ -519,8 +541,10 @@ function linkCommand(answers2) {
519
541
 
520
542
  // src/index.ts
521
543
  var TEMPLATE_DIRS = {
522
- single: fileURLToPath(new URL("../templates/next/", import.meta.url)),
523
- intl: fileURLToPath(new URL("../templates/next-intl/", import.meta.url))
544
+ lorem: fileURLToPath(new URL("../templates/lorem/", import.meta.url)),
545
+ "lorem-intl": fileURLToPath(new URL("../templates/lorem-intl/", import.meta.url)),
546
+ blank: fileURLToPath(new URL("../templates/blank/", import.meta.url)),
547
+ "blank-intl": fileURLToPath(new URL("../templates/blank-intl/", import.meta.url))
524
548
  };
525
549
  var flags;
526
550
  try {
@@ -569,7 +593,7 @@ p2.note(
569
593
  `cd ${answers.name}`,
570
594
  "npm run dev",
571
595
  "",
572
- "Site: http://localhost:3000 (the Lorem Co. starter \u2014 edit it at /admin)",
596
+ answers.starter === "blank" ? "Site: http://localhost:3000 (a blank site \u2014 build it in components/ and at /admin)" : "Site: http://localhost:3000 (the Lorem Co. starter \u2014 edit it at /admin)",
573
597
  "Admin: http://localhost:3000/admin",
574
598
  `Editor: ${outcome.editor.email ?? outcome.editor.id}`,
575
599
  ...answers.locales ? [`Locales: ${answers.locales.join(", ")} (i18n/routing.ts)`] : []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
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": {
@@ -25,7 +25,8 @@
25
25
  "build:cli": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --target=node20 --outfile=dist/index.js --banner:js='#!/usr/bin/env node'",
26
26
  "prepublishOnly": "npm run build",
27
27
  "test": "vitest run",
28
- "typecheck": "tsc --noEmit"
28
+ "typecheck": "tsc --noEmit && npm run typecheck:templates",
29
+ "typecheck:templates": "npm run build:template && tsc -p templates/lorem.tsconfig.json && tsc -p templates/lorem-intl.tsconfig.json && tsc -p templates/blank.tsconfig.json && tsc -p templates/blank-intl.tsconfig.json"
29
30
  },
30
31
  "dependencies": {
31
32
  "@clack/prompts": "^1.7.0",
@@ -0,0 +1,50 @@
1
+ # Working in this Smoodly site
2
+
3
+ This is a Next.js app with Smoodly installed: content lives in Supabase,
4
+ the admin is mounted at /admin from the `smoodly` package, and the site
5
+ reads content through `smoodly/server.ts`. Read README.md first: it says
6
+ where things are.
7
+
8
+ ## Rules
9
+
10
+ - **Register everything in `smoodly/`.** Every section, element,
11
+ collection and shared item is listed in its barrel (`sections.ts`,
12
+ `elements.ts`, `collections.ts`, `shared.ts`) and `smoodly/config.ts`
13
+ is the one registry. A component that is not registered does not exist
14
+ for the admin.
15
+ - **A new collection is a new migration.** Write its view with
16
+ `collectionSQL` from `smoodly` into a new file under
17
+ `supabase/migrations/`, then apply it: `supabase db push` (hosted) or
18
+ `supabase db reset` (local). The admin cannot list a collection whose
19
+ view is missing.
20
+ - **Seed through the stores.** `scripts/seed.ts` is starter content,
21
+ written through the page and entry stores, never raw SQL. It is
22
+ idempotent; run it with `npm run seed`.
23
+ - **Never import the admin into the site.** Nothing under `app/(site)`,
24
+ `app/[locale]` or `components/` may import `@/smoodly/admin` or
25
+ `smoodly/admin/next`; the admin's chunk must not reach visitors.
26
+ - **Two root layouts.** The site's layout lives in its route group or
27
+ locale segment and is never the admin's parent; `app/(smoodly)/admin`
28
+ is a second root layout. Do not add an `app/layout.tsx`.
29
+ - **Toolsets and elements.** A richtext field names a toolset
30
+ (`smoodly/toolsets.ts`); a toolset embeds elements by value; the config
31
+ must register those elements too. Files the toolset imports use
32
+ relative paths, not `@/`.
33
+ - **Section styles are data attributes.** `<smoodly.Section>` renders the
34
+ root element and emits the config's section styles as `data-*`
35
+ attributes; `app/globals.css` styles them. Marks
36
+ (`smoodly.marks(schema)`) tie rendered elements to fields so the
37
+ editor can select and edit them in place.
38
+ - **Multilingual sites.** Content locales are Smoodly's; URL locales are
39
+ next-intl's. The locale set is `i18n/routing.ts`. UI strings of the
40
+ site's own go through next-intl's messages in `messages/<locale>.json`.
41
+ - **Fixed versus open pages.** A page shape with a `slug` is fixed: one
42
+ record, created for you, never deleted. Without one it is open: editors
43
+ create as many as they like.
44
+
45
+ ## Commands
46
+
47
+ npm run dev # http://localhost:3000, /admin is the editor
48
+ npm run build # next build
49
+ npm run seed # starter content, idempotent
50
+ npm run typecheck # tsc --noEmit
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,75 @@
1
+ # Smoodly site
2
+
3
+ Created by `create-smoodly-app`. Content lives in Supabase; the admin is
4
+ mounted at `/admin` from the `smoodly` package. AGENTS.md holds the rules
5
+ for an agent (or a person) building the site.
6
+
7
+ npm run dev # http://localhost:3000 — /admin is the editor
8
+
9
+ ## Where things are
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.
13
+ - `app/(site)/layout.tsx` — the site's root layout: `app/globals.css`, the footer line from the `site` item; the admin has its own under `app/(smoodly)/admin/`.
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).
15
+ - `components/sections/Text.tsx` — the one section: a title and a richtext body. Listed in `smoodly/sections.ts`.
16
+ - `components/elements/Button.tsx` — the one element: a link styled as a button, embedded in richtext through `smoodly/toolsets.ts`.
17
+ - `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `shared.ts` (the site item), `sections.ts`, `elements.ts`, `toolsets.ts`, `server.ts` (stores, site layer, renderer, the read API `q`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount), `richtext.tsx` (the bound renderer).
18
+ - `scripts/seed.ts` — the home page and the site item, written through the stores. `npm run seed` is idempotent.
19
+ - `app/globals.css` — a reset, a container, the two tones. Replace it.
20
+ - `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
21
+
22
+ ## Add a section
23
+
24
+ Copy `components/sections/Text.tsx`, give the schema a new `name` and
25
+ `title`, declare its fields, render them inside `<smoodly.Section>` with
26
+ marks on each field. Add it to `sections` in `smoodly/sections.ts`, then
27
+ to the `sections` list of every zone that may hold it in
28
+ `components/pages/*.tsx`. It appears in the editor's "+" picker.
29
+
30
+ ## Add an element
31
+
32
+ Copy `components/elements/Button.tsx` (relative imports only). Add it to
33
+ `elements` in `smoodly/elements.ts` and to the `elements` of the toolset
34
+ in `smoodly/toolsets.ts` that may embed it. It appears in the richtext
35
+ editor's "/" menu.
36
+
37
+ ## Add a collection
38
+
39
+ Copy the `posts` block in `smoodly/collections.ts`, add the new
40
+ collection to `collections`, and write its entry page by copying
41
+ `components/pages/Post.tsx` (register it in `config.ts` under `pages`).
42
+ Then create a migration with its view:
43
+
44
+ import { collectionSQL } from "smoodly";
45
+ import { people } from "./smoodly/collections";
46
+ console.log(collectionSQL(people));
47
+
48
+ Save the output as `supabase/migrations/<timestamp>_people.sql` and apply
49
+ it with `supabase db push` (hosted) or `supabase db reset` (local).
50
+
51
+ ## Add a shared item
52
+
53
+ Copy the `site` block in `smoodly/shared.ts` and add it to `shared`. An
54
+ object item (`fields`) is read in code with `q().getShared("name")`; a
55
+ visual item (`section: SomeSection`) renders through its section and can
56
+ be placed on pages from the picker's Shared group. Edit both under
57
+ Globals in the admin.
58
+
59
+ Reseed an empty database any time with `npm run seed`.
60
+
61
+ ## Editors
62
+
63
+ Anyone who may open `/admin` is a Supabase Auth user with a row in the
64
+ `editors` table. To add one: create the user in Supabase Studio
65
+ (Authentication → Users), then in the SQL editor:
66
+
67
+ insert into editors (user_id, role) values ('<the user id>', 'editor');
68
+
69
+ ## Hosted Supabase
70
+
71
+ supabase init # if the CLI has not been set up here yet
72
+ supabase link # pick the project
73
+ supabase db push # applies supabase/migrations/
74
+
75
+ Then put the project's URL and keys in `.env.local`.
@@ -4,8 +4,8 @@ type Params = Promise<{ path: string[] }>;
4
4
  type SearchParams = Promise<Record<string, string | string[] | undefined>>;
5
5
 
6
6
  // Smoodly: everything else — pages at any depth and collection entries
7
- // under their `path`. Add app/<segment>/page.tsx beside this file and it
8
- // wins for that exact path; deeper paths still resolve here.
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
9
  export async function generateMetadata({ params }: { params: Params }) {
10
10
  return smoodlyMetadata((await params).path);
11
11
  }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ import { q } from "@/smoodly/server";
3
+ import "../globals.css";
4
+
5
+ // The site's root layout, in its own route group so the admin's layout
6
+ // under app/(smoodly)/admin is a second root layout beside it rather than
7
+ // a child of this one. The `site` shared item (smoodly/shared.ts) gives
8
+ // the document title and the footer line: read through `q`, cached under
9
+ // its tag, null until published.
10
+ export async function generateMetadata() {
11
+ const site = await q().getShared("site");
12
+ return { title: site?.name ?? "Smoodly site" };
13
+ }
14
+
15
+ export default async function RootLayout({ children }: { children: ReactNode }) {
16
+ const site = await q().getShared("site");
17
+ return (
18
+ <html lang="en">
19
+ <body>
20
+ <main>{children}</main>
21
+ {site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,47 @@
1
+ /* The blank starter's one stylesheet: a reset, a system font, a
2
+ container, prose defaults, the button element, and the two tones the
3
+ config's section style emits as data-tone. Replace freely. */
4
+
5
+ :root {
6
+ --paper: #ffffff;
7
+ --ink: #1a1a1a;
8
+ --muted: #666666;
9
+ --container: 720px;
10
+ }
11
+
12
+ * { box-sizing: border-box; }
13
+ html, body { margin: 0; padding: 0; }
14
+ body {
15
+ background: var(--paper);
16
+ color: var(--ink);
17
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
18
+ line-height: 1.5;
19
+ min-height: 100vh;
20
+ display: flex;
21
+ flex-direction: column;
22
+ }
23
+ main { flex: 1; }
24
+ a { color: inherit; }
25
+
26
+ .container { width: min(var(--container), 100% - 2rem); margin-inline: auto; }
27
+
28
+ /* Sections carry data-tone from the config's section styles. */
29
+ [data-tone="paper"] { background: var(--paper); color: var(--ink); }
30
+ [data-tone="ink"] { background: var(--ink); color: var(--paper); }
31
+ section[data-tone] { padding-block: 3rem; }
32
+
33
+ .prose > * + * { margin-top: 1em; }
34
+ .prose h1, .prose h2, .prose h3 { line-height: 1.2; }
35
+
36
+ /* The Button element, inside richtext. */
37
+ .button {
38
+ display: inline-block;
39
+ padding: 0.6em 1.2em;
40
+ border: 1px solid currentColor;
41
+ border-radius: 4px;
42
+ text-decoration: none;
43
+ }
44
+
45
+ .site-footer { padding-block: 2rem; color: var(--muted); font-size: 0.875rem; }
46
+ .locales { display: flex; gap: 0.75rem; padding-block: 1rem; font-size: 0.875rem; }
47
+ .locales a[aria-current="page"] { font-weight: 600; }
@@ -0,0 +1,26 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+
3
+ // An Element: a small registered block editors insert inside richtext
4
+ // (the `richtext` context). This one is a link styled as a button. Copy
5
+ // this file to add another element, then list it in smoodly/elements.ts
6
+ // and in the toolsets that may embed it (smoodly/toolsets.ts).
7
+ // Relative imports only in this file and in smoodly/toolsets.ts: the
8
+ // template build imports them outside the app, where `@/` has no mapping.
9
+ export const buttonSchema = smoodly.schema.element({
10
+ name: "button",
11
+ title: "Button",
12
+ icon: "→",
13
+ description: "A link styled as a button.",
14
+ contexts: ["richtext"],
15
+ fields: {
16
+ label: f.text().max(60),
17
+ href: f.link(),
18
+ },
19
+ sample: { label: "Read more", href: "/" },
20
+ });
21
+
22
+ function ButtonView({ label, href }: Props<typeof buttonSchema>) {
23
+ return <a className="button" href={href}>{label}</a>;
24
+ }
25
+
26
+ export const Button = smoodly.element(buttonSchema, ButtonView);
@@ -0,0 +1,22 @@
1
+ import { smoodly, z, Zone, type Props } from "smoodly";
2
+ import { Text } from "../sections/Text";
3
+
4
+ // A page shape: zones declared, <Zone /> placed, one file. `slug` makes
5
+ // it fixed — one record editors can neither delete nor duplicate — and
6
+ // the config's homePageSlug maps that record to "/". The body is a
7
+ // freeform zone over the sections listed here.
8
+ export const homeSchema = smoodly.schema.page({
9
+ name: "home",
10
+ title: "Home",
11
+ slug: "home",
12
+ zones: {
13
+ body: z.freeform({ sections: [Text] }),
14
+ },
15
+ meta: { seo: true },
16
+ });
17
+
18
+ function HomeView({ zones }: Props<typeof homeSchema>) {
19
+ return <Zone of={zones.body} />;
20
+ }
21
+
22
+ export const HomePage = smoodly.page(homeSchema, HomeView);
@@ -0,0 +1,25 @@
1
+ import { smoodly, z, Zone, type Props } from "smoodly";
2
+ import { Text } from "../sections/Text";
3
+
4
+ // An open page shape: no `slug`, so editors create as many as they like,
5
+ // anywhere in the tree the config allows. `page` is where this one sits
6
+ // (title, path, ancestors, children).
7
+ export const pageSchema = smoodly.schema.page({
8
+ name: "page",
9
+ title: "Page",
10
+ zones: {
11
+ body: z.freeform({ sections: [Text] }),
12
+ },
13
+ meta: { seo: true },
14
+ });
15
+
16
+ function PageView({ zones, page }: Props<typeof pageSchema>) {
17
+ return (
18
+ <>
19
+ <h1 className="container">{page.title}</h1>
20
+ <Zone of={zones.body} />
21
+ </>
22
+ );
23
+ }
24
+
25
+ export const StandardPage = smoodly.page(pageSchema, PageView);
@@ -0,0 +1,21 @@
1
+ import { smoodly, type EntryPageProps } from "smoodly";
2
+ import { RichText } from "@/smoodly/richtext";
3
+ import { posts } from "@/smoodly/collections";
4
+
5
+ // The entry page for `posts`, served at /posts/<slug>. It receives the
6
+ // entry's fields; the body is richtext rendered by the site's bound
7
+ // RichText, which knows the registered elements.
8
+ type PostFields = { title: string; date?: string; body?: unknown };
9
+
10
+ function PostView({ entry }: EntryPageProps) {
11
+ const post = entry.fields as PostFields;
12
+ return (
13
+ <article className="container prose">
14
+ <h1>{post.title}</h1>
15
+ {post.date && <p><time dateTime={post.date}>{post.date.slice(0, 10)}</time></p>}
16
+ <RichText doc={post.body} />
17
+ </article>
18
+ );
19
+ }
20
+
21
+ export const PostPage = smoodly.entryPage(posts, PostView);
@@ -0,0 +1,39 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+ import { RichText } from "@/smoodly/richtext";
3
+ import { text } from "@/smoodly/toolsets";
4
+
5
+ // A section: a title and a richtext body. Registered in
6
+ // smoodly/sections.ts and allowed in the page shapes' zones
7
+ // (components/pages/*.tsx). Copy this file to add another section.
8
+ // The section wrapper renders the root element and emits the config's
9
+ // section styles as data attributes (data-tone here) for app/globals.css.
10
+ export const textSchema = smoodly.schema.section({
11
+ name: "text",
12
+ title: "Text",
13
+ description: "A title and a body of text.",
14
+ fields: {
15
+ title: f.text(),
16
+ body: f.richtext({ toolset: text }),
17
+ },
18
+ sample: {
19
+ title: "Hello",
20
+ body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Write something here." }] }] },
21
+ },
22
+ });
23
+
24
+ // Marks tie the rendered elements to their fields, so the editor can
25
+ // select and inline-edit them on the canvas.
26
+ const m = smoodly.marks(textSchema);
27
+
28
+ function TextView({ title, body, styles }: Props<typeof textSchema>) {
29
+ return (
30
+ <smoodly.Section styles={styles}>
31
+ <div className="container prose">
32
+ <h2 {...m.field("title")}>{title}</h2>
33
+ <div {...m.field("body")}><RichText doc={body} /></div>
34
+ </div>
35
+ </smoodly.Section>
36
+ );
37
+ }
38
+
39
+ export const Text = smoodly.section(textSchema, TextView);
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.14"
17
+ "smoodly": "0.0.15"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",