create-smoodly-app 0.0.37 → 0.0.39
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/dist/index.js +64 -31
- package/package.json +1 -1
- package/templates/blank/AGENTS.md +7 -5
- package/templates/blank/README.md +3 -3
- package/templates/blank/_gitignore +1 -0
- package/templates/blank/package.json +2 -2
- package/templates/blank/scripts/seed.ts +45 -47
- package/templates/blank/smoodly/collections.ts +1 -1
- package/templates/blank/smoodly/ops.ts +2 -2
- package/templates/blank/smoodly/server.ts +22 -11
- package/templates/blank-intl/AGENTS.md +7 -5
- package/templates/blank-intl/README.md +3 -3
- package/templates/blank-intl/_gitignore +1 -0
- package/templates/blank-intl/package.json +2 -2
- package/templates/blank-intl/scripts/seed.ts +45 -47
- package/templates/blank-intl/smoodly/collections.ts +1 -1
- package/templates/blank-intl/smoodly/ops.ts +2 -2
- package/templates/blank-intl/smoodly/server.ts +22 -11
- package/templates/lorem/AGENTS.md +7 -5
- package/templates/lorem/README.md +2 -2
- package/templates/lorem/_gitignore +1 -0
- package/templates/lorem/package.json +2 -2
- package/templates/lorem/scripts/seed.ts +118 -131
- package/templates/lorem/smoodly/ops.ts +2 -2
- package/templates/lorem/smoodly/server.ts +22 -11
- package/templates/lorem-intl/AGENTS.md +7 -5
- package/templates/lorem-intl/README.md +2 -2
- package/templates/lorem-intl/_gitignore +1 -0
- package/templates/lorem-intl/package.json +2 -2
- package/templates/lorem-intl/scripts/seed.ts +118 -131
- package/templates/lorem-intl/smoodly/ops.ts +2 -2
- package/templates/lorem-intl/smoodly/server.ts +22 -11
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev",
|
|
7
|
-
"build": "next build",
|
|
7
|
+
"build": "smoodly migrate --check && next build",
|
|
8
8
|
"start": "next start",
|
|
9
9
|
"seed": "tsx scripts/seed.ts",
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"next": "^16.3.4",
|
|
15
15
|
"react": "^19",
|
|
16
16
|
"react-dom": "^19",
|
|
17
|
-
"smoodly": "0.0.
|
|
17
|
+
"smoodly": "0.0.39",
|
|
18
18
|
"next-intl": "^4.14.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
// The starter's content, written through the
|
|
2
|
-
//
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The starter's content, written through the content API (`content()`
|
|
2
|
+
// from smoodly/server.ts): the admin's own operations, so validation and
|
|
3
|
+
// the publish checks apply and a failure throws. It is also the example
|
|
4
|
+
// of writing content from code. Ships in the scaffold; create-smoodly-app
|
|
5
|
+
// runs `npm run seed` after the first editor. Idempotent: exits once the
|
|
6
|
+
// home page has content in the default locale. Writes the same copy into every configured locale, whatever
|
|
6
7
|
// their codes, so every locale's "/" resolves.
|
|
7
8
|
// Run: npm run seed (needs .env.local and a Supabase stack).
|
|
8
9
|
import next from "@next/env";
|
|
9
|
-
import type { PageTree } from "smoodly";
|
|
10
|
+
import type { PageTree, TreeNode } from "smoodly";
|
|
10
11
|
const { loadEnvConfig } = next;
|
|
11
12
|
|
|
12
13
|
loadEnvConfig(process.cwd());
|
|
13
14
|
|
|
14
15
|
const { config } = await import("../smoodly/config");
|
|
15
|
-
const {
|
|
16
|
-
const {
|
|
16
|
+
const { content, node } = await import("../smoodly/server");
|
|
17
|
+
const { Text } = await import("../components/sections/Text");
|
|
18
|
+
const c = content();
|
|
17
19
|
|
|
18
20
|
const DEFAULT = config.locales.default;
|
|
19
21
|
const OTHERS = config.locales.supported.filter((l) => l !== DEFAULT);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// The home page is code-owned: `find` returns its record, creating it when
|
|
24
|
+
// the database is new. It is seeded once somebody has given it sections.
|
|
25
|
+
const home = (await c.pages.find(DEFAULT, "/"))!;
|
|
26
|
+
if (Object.values((await c.pages.getTree(home.id, DEFAULT))?.zones ?? {}).some((nodes) => nodes.length > 0)) {
|
|
27
|
+
console.log(`Already seeded — the home page has content in "${DEFAULT}".`);
|
|
23
28
|
process.exit(0);
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -31,54 +36,47 @@ const doc = (...content: Node[]) => ({ type: "doc", content });
|
|
|
31
36
|
|
|
32
37
|
// --- the site item: name and tagline, published in every locale ------
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
await
|
|
36
|
-
|
|
37
|
-
await entries.addLocale("site", site.id, locale, { from: DEFAULT });
|
|
38
|
-
await entries.setStatus("site", site.id, locale, "published");
|
|
39
|
+
for (const locale of [DEFAULT, ...OTHERS]) {
|
|
40
|
+
await c.globals.set("site", locale, { name: "My site", tagline: "Built with Smoodly" });
|
|
41
|
+
await c.globals.publish("site", locale);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
// --- the home page: one Text section and one custom section ----------
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
// The custom section is not a registered section, so it is a plain node object.
|
|
47
|
+
const custom: TreeNode = {
|
|
48
|
+
id: "node-custom",
|
|
49
|
+
type: "custom",
|
|
50
|
+
fields: { rows: [
|
|
51
|
+
{ spans: [6, 6], gap: "md", align: "top", reverse: false, columns: [
|
|
52
|
+
doc(p("A custom section holds rows of columns. Drag the width bar in the inspector to resize them.")),
|
|
53
|
+
doc(p("Each column is richtext. Columns stack on small screens; the developer's stylesheet needs nothing.")),
|
|
54
|
+
] },
|
|
55
|
+
] },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const tree: PageTree = {
|
|
44
59
|
meta: { description: "A new site, built with Smoodly." },
|
|
45
60
|
zones: {
|
|
46
61
|
body: [
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
p("Everything you see is defined in components/ and registered in smoodly/. The README says where things are."),
|
|
56
|
-
button("Open the admin", "/admin"),
|
|
57
|
-
),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
62
|
+
node(Text, {
|
|
63
|
+
title: "Hello",
|
|
64
|
+
body: doc(
|
|
65
|
+
p("This is your home page. Open the admin to edit this text, add sections, create pages and write posts."),
|
|
66
|
+
p("Everything you see is defined in components/ and registered in smoodly/. The README says where things are."),
|
|
67
|
+
button("Open the admin", "/admin"),
|
|
68
|
+
),
|
|
69
|
+
}, { id: "node-hello", settings: { background: "paper" } }),
|
|
60
70
|
// The custom section: the editor's own rows of columns, rendered by Smoodly.
|
|
61
|
-
|
|
62
|
-
id: "node-custom",
|
|
63
|
-
type: "custom",
|
|
64
|
-
fields: { rows: [
|
|
65
|
-
{ spans: [6, 6], gap: "md", align: "top", reverse: false, columns: [
|
|
66
|
-
doc(p("A custom section holds rows of columns. Drag the width bar in the inspector to resize them.")),
|
|
67
|
-
doc(p("Each column is richtext. Columns stack on small screens; the developer's stylesheet needs nothing.")),
|
|
68
|
-
] },
|
|
69
|
-
] },
|
|
70
|
-
},
|
|
71
|
+
custom,
|
|
71
72
|
],
|
|
72
73
|
},
|
|
73
|
-
}
|
|
74
|
+
};
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await pages.
|
|
78
|
-
|
|
79
|
-
await pages.addLocale(home.id, locale, { from: DEFAULT, title: "Home" });
|
|
80
|
-
await pages.saveDraft(home.id, locale, tree);
|
|
81
|
-
await pages.publish(home.id, locale);
|
|
76
|
+
// The home page exists in every locale already (its registration says so): write the tree and publish.
|
|
77
|
+
for (const locale of [DEFAULT, ...OTHERS]) {
|
|
78
|
+
await c.pages.setTree(home.id, locale, tree);
|
|
79
|
+
await c.pages.publish(home.id, locale);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
console.log(`Seeded the blank starter in "${[DEFAULT, ...OTHERS].join('", "')}": the home page and the site item, published.`);
|
|
@@ -6,7 +6,7 @@ import { text } from "./toolsets";
|
|
|
6
6
|
// .localized() are stored per locale; the rest are shared across
|
|
7
7
|
// locales. To add a collection: copy this block, add it to
|
|
8
8
|
// `collections`, run `npx smoodly migrate` and apply the file it writes
|
|
9
|
-
// (supabase db push, or supabase
|
|
9
|
+
// (supabase db push, or supabase migration up locally).
|
|
10
10
|
export const posts = smoodly.collection({
|
|
11
11
|
name: "posts",
|
|
12
12
|
title: "Posts",
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// async functions — hence the shape.)
|
|
5
5
|
import { createAdminOpHandler, type AdminOpCall } from "smoodly/admin/next";
|
|
6
6
|
import { config } from "./config";
|
|
7
|
-
import {
|
|
7
|
+
import { adminDeps } from "./server";
|
|
8
8
|
|
|
9
9
|
export async function smoodlyOp(call: AdminOpCall) {
|
|
10
|
-
return createAdminOpHandler({ config, ...
|
|
10
|
+
return createAdminOpHandler({ config, ...adminDeps() })(call);
|
|
11
11
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Server-only glue: the client, the stores, the site layer and the
|
|
2
2
|
// renderer, constructed lazily so `next build` doesn't need env vars at
|
|
3
|
-
// import time. The env shape is the package's (SMOODLY_*)
|
|
3
|
+
// import time. The env shape is the package's (SMOODLY_*): the live site
|
|
4
|
+
// needs SMOODLY_URL and SMOODLY_SECRET_KEY, the admin also
|
|
5
|
+
// SMOODLY_PUBLISHABLE_KEY (the `smoodlyEnv("admin")` calls below); whether the
|
|
4
6
|
// secret is a service-role key or a cloud write key is invisible here.
|
|
5
7
|
//
|
|
6
8
|
// Lazy on purpose, the site and renderer included: config.ts imports the
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
// The admin gate from `smoodly/admin`, which holds no client code: the site
|
|
15
17
|
// routes import this file, and `smoodly/admin/next` would put the admin
|
|
16
18
|
// shell in every live page's JavaScript.
|
|
17
|
-
import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin";
|
|
19
|
+
import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
|
|
18
20
|
import { createPageRenderer, createSmoodlyQueries, type PageRegistration } from "smoodly/next";
|
|
19
21
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
20
22
|
import { getPathname } from "@/i18n/navigation";
|
|
@@ -51,8 +53,7 @@ function bound(): Bound {
|
|
|
51
53
|
// The admission table, read by the Members page (spec 2026-09-25 §4).
|
|
52
54
|
const members = new SupabaseMemberStore(db);
|
|
53
55
|
const auth = supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db });
|
|
54
|
-
//
|
|
55
|
-
// takes this local closure, not the export, so it never re-enters `bound()`.
|
|
56
|
+
// The site layer's own view of the stores; nothing outside this file reads them.
|
|
56
57
|
const stores = () => ({ pages, entries, assets });
|
|
57
58
|
// The site layer: (locale, path) → page or entry, and the content API
|
|
58
59
|
// `q` wraps below. Not exported — the app reads through `q`, which adds
|
|
@@ -71,27 +72,37 @@ function bound(): Bound {
|
|
|
71
72
|
return cached;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
/** The admin's wiring, for `smoodly/ops.ts` only. Scripts write through `content()`. */
|
|
76
|
+
export function adminDeps() {
|
|
77
|
+
smoodlyEnv("admin"); // the admin signs in with the publishable key: a missing one is named here, not on a live page
|
|
78
|
+
const { pages, entries, assets, members, auth } = bound();
|
|
79
|
+
return { pages, entries, assets, members, auth, href };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Write content from code — seeds, migrations, route handlers (spec 2026-09-30 tester findings §1).
|
|
83
|
+
* The admin's own operations with no signed-in editor: validation and publish checks apply, a failure
|
|
84
|
+
* throws `SmoodlyContentError`. `content().pages.create(…)`, `.entries.create(…)`, `.globals.set(…)`,
|
|
85
|
+
* `.media.upload(…)`; build tree nodes with `node(Section, fields)`. */
|
|
86
|
+
export function content(): SmoodlyContent {
|
|
75
87
|
const { pages, entries, assets, members } = bound();
|
|
76
|
-
return { pages, entries, assets, members };
|
|
88
|
+
return createSmoodlyContent({ config, pages, entries, assets, members, href });
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
export { node } from "smoodly";
|
|
92
|
+
|
|
79
93
|
/** The server-side client (seed script, one-off admin tasks). */
|
|
80
94
|
export const client = () => bound().db;
|
|
81
95
|
|
|
82
|
-
/** The admin gate: session verified against the auth project, editors row read with the write key. */
|
|
83
|
-
export const adminAuth = () => bound().auth;
|
|
84
|
-
|
|
85
96
|
/** What the browser needs to sign in — public values only. */
|
|
86
97
|
export function publicAuth(): AuthConfig {
|
|
87
|
-
const env = smoodlyEnv();
|
|
98
|
+
const env = smoodlyEnv("admin");
|
|
88
99
|
return { url: env.authUrl, key: env.authKey };
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
|
|
92
103
|
* The space is asked of the database once per process. */
|
|
93
104
|
export function presence(): Promise<PresenceConfig> {
|
|
94
|
-
return presenceConfig(smoodlyEnv());
|
|
105
|
+
return presenceConfig(smoodlyEnv("admin"));
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
/** Locale-relative CMS path → site URL: the one seam for the renderer
|
|
@@ -13,14 +13,16 @@ where things are.
|
|
|
13
13
|
is the one registry. A component that is not registered does not exist
|
|
14
14
|
for the admin.
|
|
15
15
|
- **After a new collection or a `smoodly` upgrade, run `npx smoodly migrate`**
|
|
16
|
-
and apply what it writes (`supabase db push` hosted, `supabase
|
|
16
|
+
and apply what it writes (`supabase db push` hosted, `supabase migration up`
|
|
17
17
|
local). It copies the core schema files the package ships and writes a
|
|
18
18
|
read view and order index per collection. Never edit a file it wrote.
|
|
19
19
|
The admin reads the base tables, so it lists a collection either way;
|
|
20
20
|
the view is for SQL and PostgREST readers, the index for `order`.
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
- **Content from code goes through `content()`.** `scripts/seed.ts` is
|
|
22
|
+
starter content written with `content()` from `smoodly/server.ts`:
|
|
23
|
+
the admin's own operations, so validation and publish checks apply and
|
|
24
|
+
a failure throws. Never write rows or call the stores directly. The
|
|
25
|
+
seed is idempotent; run it with `npm run seed`.
|
|
24
26
|
- **Never import the admin into the site.** Nothing under `app/(site)`,
|
|
25
27
|
`app/[locale]` or `components/`, and nothing they import
|
|
26
28
|
(`smoodly/server.ts` included), may import `@/smoodly/admin` or
|
|
@@ -50,6 +52,6 @@ where things are.
|
|
|
50
52
|
## Commands
|
|
51
53
|
|
|
52
54
|
npm run dev # http://localhost:3000, /admin is the editor
|
|
53
|
-
npm run build # next build
|
|
55
|
+
npm run build # checks the schema files (smoodly migrate --check), then next build
|
|
54
56
|
npm run seed # starter content, idempotent
|
|
55
57
|
npm run typecheck # tsc --noEmit
|
|
@@ -16,9 +16,9 @@ for an agent (or a person) building the site.
|
|
|
16
16
|
- `components/sections/` — the building blocks editors compose with, and the header, footer and quote behind the global items. Add one, list it in `smoodly/sections.ts`.
|
|
17
17
|
- `components/strings.ts`, `messages/` — the site's own UI strings and the request locale, one seam.
|
|
18
18
|
- `public/images/` — the starter's photos; image fields hold URLs like `/images/hero_image.jpg`.
|
|
19
|
-
- `scripts/seed.ts` — the starter content, written through the
|
|
19
|
+
- `scripts/seed.ts` — the starter content, written through the content API (`content()`). `npm run seed` is idempotent.
|
|
20
20
|
- `smoodly/collections.ts` — structured content (people, articles, news). Add a collection, run `npx smoodly migrate`, apply the file it writes.
|
|
21
|
-
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts`, `sections.ts`, `server.ts` (
|
|
21
|
+
- `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts`, `sections.ts`, `server.ts` (site layer, renderer, the content API `content()`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount).
|
|
22
22
|
- `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
|
|
23
23
|
|
|
24
24
|
## What is in the starter
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev",
|
|
7
|
-
"build": "next build",
|
|
7
|
+
"build": "smoodly migrate --check && next build",
|
|
8
8
|
"start": "next start",
|
|
9
9
|
"seed": "tsx scripts/seed.ts",
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"next": "^16.3.4",
|
|
15
15
|
"react": "^19",
|
|
16
16
|
"react-dom": "^19",
|
|
17
|
-
"smoodly": "0.0.
|
|
17
|
+
"smoodly": "0.0.39"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@next/env": "^16.3.4",
|