create-smoodly-app 0.0.50 → 0.0.52

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 (159) hide show
  1. package/README.md +22 -30
  2. package/dist/index.js +87 -329
  3. package/package.json +4 -5
  4. package/templates/astro-blank/AGENTS.md +2 -8
  5. package/templates/astro-blank/README.md +13 -23
  6. package/templates/astro-blank/package.json +2 -3
  7. package/templates/astro-blank/scripts/seed.ts +9 -3
  8. package/templates/astro-blank/src/smoodly/collections.ts +2 -2
  9. package/templates/astro-blank-intl/AGENTS.md +2 -8
  10. package/templates/astro-blank-intl/README.md +13 -23
  11. package/templates/astro-blank-intl/package.json +2 -3
  12. package/templates/astro-blank-intl/scripts/seed.ts +9 -3
  13. package/templates/astro-blank-intl/src/smoodly/collections.ts +2 -2
  14. package/templates/astro-lorem/AGENTS.md +2 -8
  15. package/templates/astro-lorem/README.md +12 -16
  16. package/templates/astro-lorem/package.json +2 -3
  17. package/templates/astro-lorem/scripts/seed.ts +9 -3
  18. package/templates/astro-lorem/src/components/sections/PageHero.schema.ts +3 -1
  19. package/templates/astro-lorem/src/sections/pageHero.astro +1 -1
  20. package/templates/astro-lorem-intl/AGENTS.md +2 -8
  21. package/templates/astro-lorem-intl/README.md +12 -16
  22. package/templates/astro-lorem-intl/package.json +2 -3
  23. package/templates/astro-lorem-intl/scripts/seed.ts +9 -3
  24. package/templates/astro-lorem-intl/src/components/sections/PageHero.schema.ts +3 -1
  25. package/templates/astro-lorem-intl/src/sections/pageHero.astro +1 -1
  26. package/templates/blank/AGENTS.md +7 -13
  27. package/templates/blank/README.md +12 -22
  28. package/templates/blank/app/(smoodly)/admin/[[...segments]]/route.ts +6 -5
  29. package/templates/blank/next.config.ts +1 -4
  30. package/templates/blank/package.json +4 -4
  31. package/templates/blank/scripts/seed.ts +9 -3
  32. package/templates/blank/smoodly/collections.ts +2 -2
  33. package/templates/blank/smoodly/server.ts +25 -47
  34. package/templates/blank-intl/AGENTS.md +7 -13
  35. package/templates/blank-intl/README.md +12 -22
  36. package/templates/blank-intl/app/(smoodly)/admin/[[...segments]]/route.ts +6 -5
  37. package/templates/blank-intl/next.config.ts +1 -4
  38. package/templates/blank-intl/package.json +4 -4
  39. package/templates/blank-intl/scripts/seed.ts +9 -3
  40. package/templates/blank-intl/smoodly/collections.ts +2 -2
  41. package/templates/blank-intl/smoodly/server.ts +25 -47
  42. package/templates/blank-intl.tsconfig.json +0 -3
  43. package/templates/blank.tsconfig.json +0 -3
  44. package/templates/lorem/AGENTS.md +7 -13
  45. package/templates/lorem/README.md +13 -17
  46. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/route.ts +6 -5
  47. package/templates/lorem/components/sections/PageHero.schema.ts +3 -1
  48. package/templates/lorem/components/sections/PageHero.tsx +1 -1
  49. package/templates/lorem/next.config.ts +1 -4
  50. package/templates/lorem/package.json +4 -4
  51. package/templates/lorem/scripts/seed.ts +9 -3
  52. package/templates/lorem/smoodly/server.ts +25 -47
  53. package/templates/lorem-intl/AGENTS.md +7 -13
  54. package/templates/lorem-intl/README.md +13 -17
  55. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/route.ts +6 -5
  56. package/templates/lorem-intl/components/sections/PageHero.schema.ts +3 -1
  57. package/templates/lorem-intl/components/sections/PageHero.tsx +1 -1
  58. package/templates/lorem-intl/next.config.ts +1 -4
  59. package/templates/lorem-intl/package.json +4 -4
  60. package/templates/lorem-intl/scripts/seed.ts +9 -3
  61. package/templates/lorem-intl/smoodly/server.ts +25 -47
  62. package/templates/lorem-intl.tsconfig.json +0 -3
  63. package/templates/lorem.tsconfig.json +0 -3
  64. package/templates/astro-blank/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  65. package/templates/astro-blank/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  66. package/templates/astro-blank/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  67. package/templates/astro-blank/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  68. package/templates/astro-blank/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  69. package/templates/astro-blank/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  70. package/templates/astro-blank/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  71. package/templates/astro-blank/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  72. package/templates/astro-blank/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  73. package/templates/astro-blank/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  74. package/templates/astro-blank/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  75. package/templates/astro-blank/supabase/migrations/20261009120100_smoodly_views.sql +0 -20
  76. package/templates/astro-blank-intl/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  77. package/templates/astro-blank-intl/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  78. package/templates/astro-blank-intl/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  79. package/templates/astro-blank-intl/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  80. package/templates/astro-blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  81. package/templates/astro-blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  82. package/templates/astro-blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  83. package/templates/astro-blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  84. package/templates/astro-blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  85. package/templates/astro-blank-intl/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  86. package/templates/astro-blank-intl/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  87. package/templates/astro-blank-intl/supabase/migrations/20261009120100_smoodly_views.sql +0 -20
  88. package/templates/astro-lorem/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  89. package/templates/astro-lorem/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  90. package/templates/astro-lorem/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  91. package/templates/astro-lorem/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  92. package/templates/astro-lorem/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  93. package/templates/astro-lorem/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  94. package/templates/astro-lorem/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  95. package/templates/astro-lorem/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  96. package/templates/astro-lorem/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  97. package/templates/astro-lorem/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  98. package/templates/astro-lorem/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  99. package/templates/astro-lorem/supabase/migrations/20261009120100_smoodly_views.sql +0 -58
  100. package/templates/astro-lorem-intl/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  101. package/templates/astro-lorem-intl/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  102. package/templates/astro-lorem-intl/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  103. package/templates/astro-lorem-intl/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  104. package/templates/astro-lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  105. package/templates/astro-lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  106. package/templates/astro-lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  107. package/templates/astro-lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  108. package/templates/astro-lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  109. package/templates/astro-lorem-intl/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  110. package/templates/astro-lorem-intl/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  111. package/templates/astro-lorem-intl/supabase/migrations/20261009120100_smoodly_views.sql +0 -58
  112. package/templates/blank/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  113. package/templates/blank/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  114. package/templates/blank/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  115. package/templates/blank/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  116. package/templates/blank/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  117. package/templates/blank/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  118. package/templates/blank/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  119. package/templates/blank/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  120. package/templates/blank/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  121. package/templates/blank/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  122. package/templates/blank/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  123. package/templates/blank/supabase/migrations/20261009120100_smoodly_views.sql +0 -20
  124. package/templates/blank-intl/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  125. package/templates/blank-intl/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  126. package/templates/blank-intl/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  127. package/templates/blank-intl/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  128. package/templates/blank-intl/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  129. package/templates/blank-intl/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  130. package/templates/blank-intl/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  131. package/templates/blank-intl/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  132. package/templates/blank-intl/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  133. package/templates/blank-intl/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  134. package/templates/blank-intl/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  135. package/templates/blank-intl/supabase/migrations/20261009120100_smoodly_views.sql +0 -20
  136. package/templates/lorem/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  137. package/templates/lorem/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  138. package/templates/lorem/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  139. package/templates/lorem/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  140. package/templates/lorem/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  141. package/templates/lorem/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  142. package/templates/lorem/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  143. package/templates/lorem/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  144. package/templates/lorem/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  145. package/templates/lorem/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  146. package/templates/lorem/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  147. package/templates/lorem/supabase/migrations/20261009120100_smoodly_views.sql +0 -58
  148. package/templates/lorem-intl/supabase/migrations/20260926120000_smoodly_v1.sql +0 -535
  149. package/templates/lorem-intl/supabase/migrations/20260926130000_smoodly_v2.sql +0 -26
  150. package/templates/lorem-intl/supabase/migrations/20260926140000_smoodly_v3.sql +0 -38
  151. package/templates/lorem-intl/supabase/migrations/20260926150000_smoodly_v4.sql +0 -41
  152. package/templates/lorem-intl/supabase/migrations/20260929120000_smoodly_v5.sql +0 -56
  153. package/templates/lorem-intl/supabase/migrations/20260929135523_smoodly_v6.sql +0 -200
  154. package/templates/lorem-intl/supabase/migrations/20260930040956_smoodly_v7.sql +0 -84
  155. package/templates/lorem-intl/supabase/migrations/20261007120000_smoodly_v8.sql +0 -154
  156. package/templates/lorem-intl/supabase/migrations/20261007200000_smoodly_v9.sql +0 -30
  157. package/templates/lorem-intl/supabase/migrations/20261008120000_smoodly_v10.sql +0 -47
  158. package/templates/lorem-intl/supabase/migrations/20261009120000_smoodly_v11.sql +0 -49
  159. package/templates/lorem-intl/supabase/migrations/20261009120100_smoodly_views.sql +0 -58
@@ -2,9 +2,9 @@
2
2
  // from smoodly/astro): the admin's own operations, so validation and
3
3
  // the publish checks apply and a failure throws. This file is also the
4
4
  // example of writing content from code. Content only; it ships in the template and create-smoodly-app runs
5
- // `npm run seed` after the first editor. (The example repo's demo
5
+ // `npm run seed` after it pushes the schema. (The example repo's demo
6
6
  // editor account is a separate, example-only script.)
7
- // Run: npm run seed (needs .env and a Supabase stack).
7
+ // Run: npm run seed (needs .env).
8
8
  //
9
9
  // Locales: the English content goes into the configured default locale,
10
10
  // whatever its code, and the Finnish rows are written only when `fi` is
@@ -17,6 +17,12 @@
17
17
  // page has content.
18
18
  import type { PageTree, TreeNode } from "smoodly";
19
19
 
20
+ // A refusal from Smoodly's API (a space with no schema yet, a wrong key) prints its message and code, not a stack trace.
21
+ process.on("uncaughtException", (e) => {
22
+ console.error(e.name === "SmoodlyContentError" ? `${e.message} (${(e as Error & { code?: string }).code})` : e);
23
+ process.exit(1);
24
+ });
25
+
20
26
  const { config } = await import("../src/smoodly/config");
21
27
  const { articles, news } = await import("../src/smoodly/collections");
22
28
  const { node } = await import("smoodly");
@@ -34,7 +40,7 @@ const EN = config.locales.default;
34
40
  const FI = config.locales.supported.includes("fi") && EN !== "fi" ? "fi" : null;
35
41
 
36
42
  // The home page is code-owned: `find` returns its record, creating it when
37
- // the database is new. It is seeded once somebody has given it sections.
43
+ // the space is new. It is seeded once somebody has given it sections.
38
44
  const home = (await c.pages.find(EN, "/"))!;
39
45
  if (Object.values((await c.pages.getTree(home.id, EN))?.zones ?? {}).some((nodes) => nodes.length > 0)) {
40
46
  console.log(`Already seeded — the home page has content in "${EN}".`);
@@ -4,6 +4,8 @@ import { f, smoodly } from "smoodly";
4
4
  // text and an optional wide photo. Page and Articles lock it — each page
5
5
  // shape locks its own hero, which is the point: a locked zone is per
6
6
  // shape. Without an image the copy stands alone (the Articles index).
7
+ // The photo has two named crops: wide on a desktop, tall on a phone; the
8
+ // view says which applies where.
7
9
  export const pageHeroSchema = smoodly.schema.section({
8
10
  name: "pageHero",
9
11
  title: "Page hero",
@@ -12,7 +14,7 @@ export const pageHeroSchema = smoodly.schema.section({
12
14
  kicker: f.text().optional(),
13
15
  heading: f.text(),
14
16
  text: f.text(),
15
- image: f.image({ aspect: "21:9" }).optional(),
17
+ image: f.image({ crops: { wide: "21:9", tall: "4:5" } }).optional(),
16
18
  },
17
19
  sample: {
18
20
  heading: "About us",
@@ -15,6 +15,6 @@ const m = smoodly.marks(pageHeroSchema);
15
15
  <h1 class="page-hero__heading" {...m.field("heading")}>{heading}</h1>
16
16
  <p class="page-hero__text" {...m.field("text")}>{text}</p>
17
17
  </div>
18
- {image && <Image image={image} class="page-hero__image" sizes="100vw" priority {...m.field("image")} />}
18
+ {image && <Image image={image} media={{ tall: "(max-width: 767px)" }} class="page-hero__image" sizes="100vw" priority {...m.field("image")} />}
19
19
  </div>
20
20
  </Section>
@@ -1,9 +1,9 @@
1
1
  # Working in this Smoodly site
2
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.
3
+ This is a Next.js app with Smoodly installed: content lives in Smoodly's service,
4
+ reached through the API, the admin is mounted at /admin from the `smoodly`
5
+ package, and the site reads content through `smoodly/server.ts`. Read
6
+ README.md first: it says where things are.
7
7
 
8
8
  ## Rules
9
9
 
@@ -20,12 +20,6 @@ where things are.
20
20
  `smoodly/server.ts` or `smoodly/richtext.tsx`. Zones, toolsets, global
21
21
  items and `node()` take schemas (`z.locked(heroSchema)`), never
22
22
  components.
23
- - **After a new collection or a `smoodly` upgrade, run `npx smoodly migrate`**
24
- and apply what it writes (`supabase db push` hosted, `supabase migration up`
25
- local). It copies the core schema files the package ships and writes a
26
- read view and order index per collection. Never edit a file it wrote.
27
- The admin reads the base tables, so it lists a collection either way;
28
- the view is for SQL and PostgREST readers, the index for `order`.
29
23
  - **Content from code goes through `content()`.** `scripts/seed.ts` is
30
24
  starter content written with `content()` from `smoodly/server.ts`:
31
25
  the admin's own operations, so validation and publish checks apply and
@@ -35,8 +29,8 @@ where things are.
35
29
  serves the admin's page, its files and its operations from the
36
30
  `smoodly` package; the folder name must match `admin.path` in the
37
31
  config. The admin is prebuilt: nothing of it is compiled into the
38
- site, and it takes no layout. Server-side pieces such as
39
- `supabaseAdminAuth` and the content API come from `smoodly/admin`.
32
+ site, and it takes no layout. The content API (`createSmoodlyContent`,
33
+ `apiTransport`) comes from `smoodly/admin`.
40
34
  - **One root layout.** The site's layout lives in its route group or
41
35
  locale segment. Do not add an `app/layout.tsx`: it would wrap that
42
36
  layout in a second `<html>`.
@@ -59,6 +53,6 @@ where things are.
59
53
  ## Commands
60
54
 
61
55
  npm run dev # http://localhost:3000, /admin is the editor
62
- npm run build # checks the schema files (smoodly migrate --check), then next build
56
+ npm run build # next build
63
57
  npm run seed # starter content, idempotent
64
58
  npm run typecheck # tsc --noEmit
@@ -1,8 +1,8 @@
1
1
  # Smoodly site
2
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.
3
+ Created by `create-smoodly-app`. Content lives in Smoodly's service; the
4
+ admin is mounted at `/admin` from the `smoodly` package. AGENTS.md holds
5
+ the rules for an agent (or a person) building the site.
6
6
 
7
7
  npm run dev # http://localhost:3000 — /admin is the editor
8
8
 
@@ -17,7 +17,7 @@ for an agent (or a person) building the site.
17
17
  - `smoodly/` — the wiring: `config.ts` (the registry of schemas and the settings; it imports no component), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts` and `elements.ts` (the schemas the config registers), `views.ts` (the components that render them), `toolsets.ts`, `server.ts` (site layer, renderer, the read API `q`, the content API `content()`), `richtext.tsx` (the bound renderer).
18
18
  - `scripts/seed.ts` — the home page and the site item, written through the content API (`content()`). `npm run seed` is idempotent.
19
19
  - `app/globals.css` — a reset, a container, the two tones. Replace it.
20
- - `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
20
+ - `.env.local` — `SMOODLY_SPACE_ID`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY` (the space's values from smoodly.io) and `SITE_URL`.
21
21
 
22
22
  ## Add a section
23
23
 
@@ -44,12 +44,6 @@ it; add the component to `elements` in `smoodly/views.ts` and in
44
44
  Copy the `posts` block in `smoodly/collections.ts`, add the new
45
45
  collection to `collections`, and write its entry page by copying
46
46
  `components/pages/Post.tsx` (list it in `smoodly/views.ts` under `pages`).
47
- Then write its read view and order index and apply them:
48
-
49
- npx smoodly migrate
50
- supabase db push # hosted; `supabase migration up` for the local stack (keeps the content)
51
-
52
- The same command brings the core schema forward after a `smoodly` upgrade.
53
47
 
54
48
  ## Add a global item
55
49
 
@@ -59,7 +53,7 @@ visual item (`section: someSectionSchema`) renders through its section and can
59
53
  be placed on pages from the picker's Global group. Edit both under
60
54
  Globals in the admin.
61
55
 
62
- Reseed an empty database any time with `npm run seed`.
56
+ Reseed an empty space any time with `npm run seed`.
63
57
 
64
58
  ## Moving the admin
65
59
 
@@ -72,16 +66,12 @@ The two must agree; the admin says so when they do not.
72
66
 
73
67
  ## Editors
74
68
 
75
- Anyone who may open `/admin` is a Supabase Auth user with a row in the
76
- `editors` table. To add one: create the user in Supabase Studio
77
- (Authentication → Users), then in the SQL editor:
78
-
79
- insert into editors (user_id, role) values ('<the user id>', 'editor');
80
-
81
- ## Hosted Supabase
69
+ Editors sign in at `/admin` with their smoodly.io account. Colleagues are
70
+ invited from the project at smoodly.io; the admin's Members page lists them.
82
71
 
83
- supabase init # if the CLI has not been set up here yet
84
- supabase link # pick the project
85
- supabase db push # applies supabase/migrations/
72
+ ## Deploying
86
73
 
87
- Then put the project's URL and keys in `.env.local`.
74
+ `.env.local` holds the preview space's values, for development. Where you
75
+ deploy, set the production space's three values and `SITE_URL` in the
76
+ host's environment variables; each build pushes the
77
+ schema to its space. https://smoodly.io/docs/getting-started/deploying
@@ -1,8 +1,9 @@
1
- // The admin: its page, the package's files and the operations endpoint, all from this one route. The
2
- // admin is prebuilt in the `smoodly` package; nothing of it is compiled into the site. The folder name
3
- // must match `admin.path` in smoodly/config.ts.
1
+ // The admin: its page, the package's files, the revalidate route Smoodly's API calls after a publish and
2
+ // the preview route, all from this one route; the operations run in Smoodly's API. The admin is prebuilt
3
+ // in the `smoodly` package; nothing of it is compiled into the site. The folder name must match
4
+ // `admin.path` in smoodly/config.ts.
4
5
  import { createAdminRoute } from "smoodly/next";
5
6
  import { config } from "@/smoodly/config";
6
- import { adminDeps, presence, publicAuth } from "@/smoodly/server";
7
+ import { adminDeps, presence } from "@/smoodly/server";
7
8
 
8
- export const { GET, POST } = createAdminRoute(() => ({ config, ...adminDeps(), signIn: publicAuth(), presence }));
9
+ export const { GET, POST } = createAdminRoute(() => ({ config, ...adminDeps(), presence }));
@@ -1,8 +1,5 @@
1
1
  import type { NextConfig } from "next";
2
- import { smoodlyImages } from "smoodly/image/config";
3
2
 
4
- // next/image may only optimize from the Supabase host named in SMOODLY_URL
5
- // (a local stack also needs Next's local-IP allowance; the helper sets it).
6
- const config: NextConfig = { images: smoodlyImages() };
3
+ const config: NextConfig = {};
7
4
 
8
5
  export default config;
@@ -4,17 +4,17 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "next dev",
7
- "build": "smoodly migrate --check && next build",
7
+ "build": "next build",
8
8
  "start": "next start",
9
9
  "seed": "tsx scripts/seed.ts",
10
- "typecheck": "tsc --noEmit"
10
+ "typecheck": "tsc --noEmit",
11
+ "prebuild": "smoodly push"
11
12
  },
12
13
  "dependencies": {
13
- "@supabase/supabase-js": "^2.113.0",
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.50"
17
+ "smoodly": "0.0.52"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -2,16 +2,22 @@
2
2
  // from smoodly/server.ts): the admin's own operations, so validation and
3
3
  // the publish checks apply and a failure throws. It is also the example
4
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
5
+ // runs `npm run seed` after it pushes the schema. Idempotent: exits once the
6
6
  // home page has content in the default locale. Writes the same copy into every configured locale, whatever
7
7
  // their codes, so every locale's "/" resolves.
8
- // Run: npm run seed (needs .env.local and a Supabase stack).
8
+ // Run: npm run seed (needs .env.local).
9
9
  import next from "@next/env";
10
10
  import type { PageTree, TreeNode } from "smoodly";
11
11
  const { loadEnvConfig } = next;
12
12
 
13
13
  loadEnvConfig(process.cwd());
14
14
 
15
+ // A refusal from Smoodly's API (a space with no schema yet, a wrong key) prints its message and code, not a stack trace.
16
+ process.on("uncaughtException", (e) => {
17
+ console.error(e.name === "SmoodlyContentError" ? `${e.message} (${(e as Error & { code?: string }).code})` : e);
18
+ process.exit(1);
19
+ });
20
+
15
21
  const { config } = await import("../smoodly/config");
16
22
  const { content, node } = await import("../smoodly/server");
17
23
  const { textSchema } = await import("../components/sections/Text.schema");
@@ -21,7 +27,7 @@ const DEFAULT = config.locales.default;
21
27
  const OTHERS = config.locales.supported.filter((l) => l !== DEFAULT);
22
28
 
23
29
  // 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.
30
+ // the space is new. It is seeded once somebody has given it sections.
25
31
  const home = (await c.pages.find(DEFAULT, "/"))!;
26
32
  if (Object.values((await c.pages.getTree(home.id, DEFAULT))?.zones ?? {}).some((nodes) => nodes.length > 0)) {
27
33
  console.log(`Already seeded — the home page has content in "${DEFAULT}".`);
@@ -5,8 +5,8 @@ import { text } from "./toolsets";
5
5
  // `path` by its entry page (components/pages/Post.tsx). Fields marked
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
- // `collections`, run `npx smoodly migrate` and apply the file it writes
9
- // (supabase db push, or supabase migration up locally).
8
+ // `collections`; the dev server and every build push the change to your
9
+ // space.
10
10
  export const posts = smoodly.collection({
11
11
  name: "posts",
12
12
  title: "Posts",
@@ -1,59 +1,46 @@
1
- // Server-only glue: the client, the stores, the site layer and the
2
- // renderer, constructed lazily so `next build` doesn't need env vars at
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
6
- // secret is a service-role key or a cloud write key is invisible here.
1
+ // Server-only glue: the reads over Smoodly's API and the renderer,
2
+ // constructed lazily so `next build` doesn't need env vars at import time.
3
+ // The env shape is the package's (SMOODLY_*): the live site and the admin need
4
+ // SMOODLY_API_URL, SMOODLY_SPACE_ID and SMOODLY_SECRET_KEY; the admin learns
5
+ // where editors sign in from Smoodly's API. The site opens no database.
7
6
  //
8
7
  // Lazy on purpose: a page component imports this file to read through
9
8
  // `q` (components/pages/Articles.tsx), and this file imports the
10
9
  // components through ./views. That is a cycle, so nothing here reads
11
10
  // views() or the env at module scope.
12
- import {
13
- createSmoodlyClient, createSmoodlySite, createSupabaseStores, presenceConfig, siteOptions, smoodlyEnv,
14
- type PresenceConfig, type SmoodlySite, type SupabaseAssetStore, type SupabaseEntryStore, type SupabaseMemberStore, type SupabasePageStore,
15
- } from "smoodly";
16
- // The admin's server pieces: the sign-in check and the content API. No browser code lives in the package's
17
- // entries; the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
18
- import { createSmoodlyContent, supabaseAdminAuth, type AdminAuth, type AuthConfig, type SmoodlyContent } from "smoodly/admin";
11
+ import { apiEnv, createSmoodlyApi, presenceConfig, smoodlyEnv, type ContentReads, type PresenceConfig } from "smoodly";
12
+ // The admin's server pieces: the content API for scripts. No browser code lives in the package's entries;
13
+ // the admin itself is prebuilt and served by app/(smoodly)/admin/[[...segments]]/route.ts.
14
+ import { apiTransport, createSmoodlyContent, type SmoodlyContent } from "smoodly/admin";
19
15
  import { buildGate, createPageRenderer, createSmoodlyQueries } from "smoodly/next";
20
- import type { SupabaseClient } from "@supabase/supabase-js";
21
16
  import { collections, config, globals } from "./config";
22
17
  import { views } from "./views";
23
18
 
24
19
  type Renderer = ReturnType<typeof createPageRenderer>;
25
20
  type Queries = ReturnType<typeof createSmoodlyQueries<typeof collections, typeof globals>>;
26
- type Bound = {
27
- db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; assets: SupabaseAssetStore; members: SupabaseMemberStore; auth: AdminAuth; site: SmoodlySite; renderer: Renderer; q: Queries;
28
- };
21
+ type Bound = { site: ContentReads; renderer: Renderer; q: Queries };
29
22
 
30
23
  let cached: Bound | null = null;
31
24
 
32
25
  function bound(): Bound {
33
26
  if (cached) return cached;
34
- const env = smoodlyEnv();
35
- const db = createSmoodlyClient(env);
36
- // The stores, read off the config: pages, entries, the media library and the admission table.
37
- const { pages, entries, assets, members, published } = createSupabaseStores(db, config);
38
- const auth = supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db });
39
- // The site layer: (locale, path) → page or entry, and the content API
40
- // `q` wraps below. Not exported — the app reads through `q`, which adds
41
- // draft awareness and the cache.
42
- const site = createSmoodlySite(siteOptions(config, () => ({ pages, entries, assets, published })));
27
+ // The site's reads go over Smoodly's API (SMOODLY_API_URL, SMOODLY_SPACE_ID, SMOODLY_SECRET_KEY):
28
+ // (locale, path) → page or entry, and the reads `q` wraps below. Not exported — the app reads
29
+ // through `q`, which adds draft awareness and the cache.
30
+ const site = createSmoodlyApi({ ...apiEnv(), config });
43
31
  // The components, checked against the config's schemas here, on first use.
44
32
  const renderer = createPageRenderer({ site, ...views(), href });
45
33
  // The read API (spec 2026-09-11): typed by the registry, draft-aware, cached under tags the admin expires.
46
34
  const q = createSmoodlyQueries({ site, collections, globals });
47
- cached = { db, pages, entries, assets, members, auth, site, renderer, q };
35
+ cached = { site, renderer, q };
48
36
  return cached;
49
37
  }
50
38
 
51
- /** The admin's wiring, for the admin route only. Scripts write through `content()`. */
39
+ /** The admin's wiring, for the admin route only: the page and its files, the revalidate route the
40
+ * service calls and the preview route the browser posts the grant to. Operations run in the service. */
52
41
  export function adminDeps() {
53
- smoodlyEnv("admin"); // the admin signs in with the publishable key: a missing one is named here, not on a live page
54
- const { pages, entries, assets, members, auth, site } = bound();
55
- // The projector: a publish writes the finished documents a live page reads.
56
- return { pages, entries, assets, members, auth, href, projector: site.projector };
42
+ const env = smoodlyEnv("admin"); // the admin talks to SMOODLY_API_URL, which also tells it where editors sign in
43
+ return { apiUrl: env.apiUrl, spaceId: env.spaceId, delivery: "live" as const };
57
44
  }
58
45
 
59
46
  /** Write content from code — seeds, migrations, route handlers (spec 2026-09-30 tester findings §1).
@@ -61,24 +48,15 @@ export function adminDeps() {
61
48
  * throws `SmoodlyContentError`. `content().pages.create(…)`, `.entries.create(…)`, `.globals.set(…)`,
62
49
  * `.media.upload(…)`; build tree nodes with `node(Section, fields)`. */
63
50
  export function content(): SmoodlyContent {
64
- const { pages, entries, assets, members, site } = bound();
65
- return createSmoodlyContent({ config, pages, entries, assets, members, href, projector: site.projector });
51
+ // Over Smoodly's API with the secret key: the service runs the operations.
52
+ return createSmoodlyContent({ config, href, transport: apiTransport(apiEnv()) });
66
53
  }
67
54
 
68
55
  export { node } from "smoodly";
69
56
 
70
- /** The server-side client (seed script, one-off admin tasks). */
71
- export const client = () => bound().db;
72
-
73
- /** What the browser needs to sign in — public values only. */
74
- export function publicAuth(): AuthConfig {
75
- const env = smoodlyEnv("admin");
76
- return { url: env.authUrl, key: env.authKey };
77
- }
78
-
79
- /** What the browser needs to join the editing-presence channel — public values only (spec 2026-09-26 concurrency §5).
80
- * The space is asked of the database once per process. */
81
- export function presence(): Promise<PresenceConfig> {
57
+ /** What the browser needs to open the editing-presence socket — Smoodly's API and the space, public values only
58
+ * (spec 2026-10-15 the space object §6). */
59
+ export function presence(): PresenceConfig {
82
60
  return presenceConfig(smoodlyEnv("admin"));
83
61
  }
84
62
 
@@ -105,7 +83,7 @@ export const q = (): Queries =>
105
83
  // route file without a dynamic segment reads nothing at build and the build needs no keys — a layout
106
84
  // reading a global through q() included (2026-10-02, tester round 6). At run time it does nothing: a
107
85
  // CMS route renders on its first visit and is cached until a publish expires it (plan 2026-10-09).
108
- /** The two route files call these. */
86
+ /** The route file calls these. */
109
87
  export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPath(...args); };
110
88
  export const smoodlyMetadata: Renderer["smoodlyMetadata"] = async (...args) => { await buildGate(); return bound().renderer.smoodlyMetadata(...args); };
111
89
  export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = async (...args) => { await buildGate(); return bound().renderer.renderSmoodlyPage(...args); };
@@ -1,9 +1,9 @@
1
1
  # Working in this Smoodly site
2
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.
3
+ This is a Next.js app with Smoodly installed: content lives in Smoodly's service,
4
+ reached through the API, the admin is mounted at /admin from the `smoodly`
5
+ package, and the site reads content through `smoodly/server.ts`. Read
6
+ README.md first: it says where things are.
7
7
 
8
8
  ## Rules
9
9
 
@@ -20,12 +20,6 @@ where things are.
20
20
  `smoodly/server.ts` or `smoodly/richtext.tsx`. Zones, toolsets, global
21
21
  items and `node()` take schemas (`z.locked(heroSchema)`), never
22
22
  components.
23
- - **After a new collection or a `smoodly` upgrade, run `npx smoodly migrate`**
24
- and apply what it writes (`supabase db push` hosted, `supabase migration up`
25
- local). It copies the core schema files the package ships and writes a
26
- read view and order index per collection. Never edit a file it wrote.
27
- The admin reads the base tables, so it lists a collection either way;
28
- the view is for SQL and PostgREST readers, the index for `order`.
29
23
  - **Content from code goes through `content()`.** `scripts/seed.ts` is
30
24
  starter content written with `content()` from `smoodly/server.ts`:
31
25
  the admin's own operations, so validation and publish checks apply and
@@ -35,8 +29,8 @@ where things are.
35
29
  serves the admin's page, its files and its operations from the
36
30
  `smoodly` package; the folder name must match `admin.path` in the
37
31
  config. The admin is prebuilt: nothing of it is compiled into the
38
- site, and it takes no layout. Server-side pieces such as
39
- `supabaseAdminAuth` and the content API come from `smoodly/admin`.
32
+ site, and it takes no layout. The content API (`createSmoodlyContent`,
33
+ `apiTransport`) comes from `smoodly/admin`.
40
34
  - **One root layout.** The site's layout lives in its route group or
41
35
  locale segment. Do not add an `app/layout.tsx`: it would wrap that
42
36
  layout in a second `<html>`.
@@ -59,6 +53,6 @@ where things are.
59
53
  ## Commands
60
54
 
61
55
  npm run dev # http://localhost:3000, /admin is the editor
62
- npm run build # checks the schema files (smoodly migrate --check), then next build
56
+ npm run build # next build
63
57
  npm run seed # starter content, idempotent
64
58
  npm run typecheck # tsc --noEmit
@@ -1,8 +1,8 @@
1
1
  # Smoodly site
2
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.
3
+ Created by `create-smoodly-app`. Content lives in Smoodly's service; the
4
+ admin is mounted at `/admin` from the `smoodly` package. AGENTS.md holds
5
+ the rules for an agent (or a person) building the site.
6
6
 
7
7
  npm run dev # http://localhost:3000 — /admin is the editor
8
8
 
@@ -18,7 +18,7 @@ for an agent (or a person) building the site.
18
18
  - `smoodly/` — the wiring: `config.ts` (the registry of schemas and the settings; it imports no component), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts` and `elements.ts` (the schemas the config registers), `views.ts` (the components that render them), `toolsets.ts`, `server.ts` (site layer, renderer, the read API `q`, the content API `content()`), `richtext.tsx` (the bound renderer).
19
19
  - `scripts/seed.ts` — the home page and the site item, written through the content API (`content()`). `npm run seed` is idempotent.
20
20
  - `app/globals.css` — a reset, a container, the two tones. Replace it.
21
- - `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
21
+ - `.env.local` — `SMOODLY_SPACE_ID`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY` (the space's values from smoodly.io) and `SITE_URL`.
22
22
 
23
23
  ## Add a section
24
24
 
@@ -45,12 +45,6 @@ it; add the component to `elements` in `smoodly/views.ts` and in
45
45
  Copy the `posts` block in `smoodly/collections.ts`, add the new
46
46
  collection to `collections`, and write its entry page by copying
47
47
  `components/pages/Post.tsx` (list it in `smoodly/views.ts` under `pages`).
48
- Then write its read view and order index and apply them:
49
-
50
- npx smoodly migrate
51
- supabase db push # hosted; `supabase migration up` for the local stack (keeps the content)
52
-
53
- The same command brings the core schema forward after a `smoodly` upgrade.
54
48
 
55
49
  ## Add a global item
56
50
 
@@ -60,7 +54,7 @@ visual item (`section: someSectionSchema`) renders through its section and can
60
54
  be placed on pages from the picker's Global group. Edit both under
61
55
  Globals in the admin.
62
56
 
63
- Reseed an empty database any time with `npm run seed`.
57
+ Reseed an empty space any time with `npm run seed`.
64
58
 
65
59
  ## Moving the admin
66
60
 
@@ -75,16 +69,12 @@ The two must agree; the admin says so when they do not.
75
69
 
76
70
  ## Editors
77
71
 
78
- Anyone who may open `/admin` is a Supabase Auth user with a row in the
79
- `editors` table. To add one: create the user in Supabase Studio
80
- (Authentication → Users), then in the SQL editor:
81
-
82
- insert into editors (user_id, role) values ('<the user id>', 'editor');
83
-
84
- ## Hosted Supabase
72
+ Editors sign in at `/admin` with their smoodly.io account. Colleagues are
73
+ invited from the project at smoodly.io; the admin's Members page lists them.
85
74
 
86
- supabase init # if the CLI has not been set up here yet
87
- supabase link # pick the project
88
- supabase db push # applies supabase/migrations/
75
+ ## Deploying
89
76
 
90
- Then put the project's URL and keys in `.env.local`.
77
+ `.env.local` holds the preview space's values, for development. Where you
78
+ deploy, set the production space's three values and `SITE_URL` in the
79
+ host's environment variables; each build pushes the
80
+ schema to its space. https://smoodly.io/docs/getting-started/deploying
@@ -1,8 +1,9 @@
1
- // The admin: its page, the package's files and the operations endpoint, all from this one route. The
2
- // admin is prebuilt in the `smoodly` package; nothing of it is compiled into the site. The folder name
3
- // must match `admin.path` in smoodly/config.ts.
1
+ // The admin: its page, the package's files, the revalidate route Smoodly's API calls after a publish and
2
+ // the preview route, all from this one route; the operations run in Smoodly's API. The admin is prebuilt
3
+ // in the `smoodly` package; nothing of it is compiled into the site. The folder name must match
4
+ // `admin.path` in smoodly/config.ts.
4
5
  import { createAdminRoute } from "smoodly/next";
5
6
  import { config } from "@/smoodly/config";
6
- import { adminDeps, presence, publicAuth } from "@/smoodly/server";
7
+ import { adminDeps, presence } from "@/smoodly/server";
7
8
 
8
- export const { GET, POST } = createAdminRoute(() => ({ config, ...adminDeps(), signIn: publicAuth(), presence }));
9
+ export const { GET, POST } = createAdminRoute(() => ({ config, ...adminDeps(), presence }));
@@ -1,11 +1,8 @@
1
1
  import type { NextConfig } from "next";
2
2
  import createNextIntlPlugin from "next-intl/plugin";
3
- import { smoodlyImages } from "smoodly/image/config";
4
3
 
5
4
  const withNextIntl = createNextIntlPlugin();
6
5
 
7
- // next/image may only optimize from the Supabase host named in SMOODLY_URL
8
- // (a local stack also needs Next's local-IP allowance; the helper sets it).
9
- const config: NextConfig = { images: smoodlyImages() };
6
+ const config: NextConfig = {};
10
7
 
11
8
  export default withNextIntl(config);
@@ -4,17 +4,17 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "next dev",
7
- "build": "smoodly migrate --check && next build",
7
+ "build": "next build",
8
8
  "start": "next start",
9
9
  "seed": "tsx scripts/seed.ts",
10
- "typecheck": "tsc --noEmit"
10
+ "typecheck": "tsc --noEmit",
11
+ "prebuild": "smoodly push"
11
12
  },
12
13
  "dependencies": {
13
- "@supabase/supabase-js": "^2.113.0",
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.50",
17
+ "smoodly": "0.0.52",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -2,16 +2,22 @@
2
2
  // from smoodly/server.ts): the admin's own operations, so validation and
3
3
  // the publish checks apply and a failure throws. It is also the example
4
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
5
+ // runs `npm run seed` after it pushes the schema. Idempotent: exits once the
6
6
  // home page has content in the default locale. Writes the same copy into every configured locale, whatever
7
7
  // their codes, so every locale's "/" resolves.
8
- // Run: npm run seed (needs .env.local and a Supabase stack).
8
+ // Run: npm run seed (needs .env.local).
9
9
  import next from "@next/env";
10
10
  import type { PageTree, TreeNode } from "smoodly";
11
11
  const { loadEnvConfig } = next;
12
12
 
13
13
  loadEnvConfig(process.cwd());
14
14
 
15
+ // A refusal from Smoodly's API (a space with no schema yet, a wrong key) prints its message and code, not a stack trace.
16
+ process.on("uncaughtException", (e) => {
17
+ console.error(e.name === "SmoodlyContentError" ? `${e.message} (${(e as Error & { code?: string }).code})` : e);
18
+ process.exit(1);
19
+ });
20
+
15
21
  const { config } = await import("../smoodly/config");
16
22
  const { content, node } = await import("../smoodly/server");
17
23
  const { textSchema } = await import("../components/sections/Text.schema");
@@ -21,7 +27,7 @@ const DEFAULT = config.locales.default;
21
27
  const OTHERS = config.locales.supported.filter((l) => l !== DEFAULT);
22
28
 
23
29
  // 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.
30
+ // the space is new. It is seeded once somebody has given it sections.
25
31
  const home = (await c.pages.find(DEFAULT, "/"))!;
26
32
  if (Object.values((await c.pages.getTree(home.id, DEFAULT))?.zones ?? {}).some((nodes) => nodes.length > 0)) {
27
33
  console.log(`Already seeded — the home page has content in "${DEFAULT}".`);
@@ -5,8 +5,8 @@ import { text } from "./toolsets";
5
5
  // `path` by its entry page (components/pages/Post.tsx). Fields marked
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
- // `collections`, run `npx smoodly migrate` and apply the file it writes
9
- // (supabase db push, or supabase migration up locally).
8
+ // `collections`; the dev server and every build push the change to your
9
+ // space.
10
10
  export const posts = smoodly.collection({
11
11
  name: "posts",
12
12
  title: "Posts",