create-smoodly-app 0.0.13 → 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/{next → blank}/smoodly/server.ts +17 -13
  22. package/templates/blank/smoodly/shared.ts +16 -0
  23. package/templates/blank/smoodly/toolsets.ts +14 -0
  24. package/templates/blank/supabase/migrations/00000000000000_smoodly.sql +518 -0
  25. package/templates/blank-intl/AGENTS.md +50 -0
  26. package/templates/blank-intl/CLAUDE.md +1 -0
  27. package/templates/blank-intl/README.md +76 -0
  28. package/templates/blank-intl/app/[locale]/layout.tsx +42 -0
  29. package/templates/blank-intl/app/globals.css +47 -0
  30. package/templates/blank-intl/components/elements/Button.tsx +26 -0
  31. package/templates/blank-intl/components/pages/Home.tsx +22 -0
  32. package/templates/blank-intl/components/pages/Page.tsx +25 -0
  33. package/templates/blank-intl/components/pages/Post.tsx +21 -0
  34. package/templates/blank-intl/components/sections/Text.tsx +39 -0
  35. package/templates/blank-intl/messages/en.json +1 -0
  36. package/templates/{next-intl → blank-intl}/package.json +1 -1
  37. package/templates/blank-intl/scripts/seed.ts +72 -0
  38. package/templates/blank-intl/smoodly/collections.ts +26 -0
  39. package/templates/blank-intl/smoodly/config.ts +38 -0
  40. package/templates/blank-intl/smoodly/elements.ts +5 -0
  41. package/templates/blank-intl/smoodly/sections.ts +5 -0
  42. package/templates/{next-intl → blank-intl}/smoodly/server.ts +17 -13
  43. package/templates/blank-intl/smoodly/shared.ts +16 -0
  44. package/templates/blank-intl/smoodly/toolsets.ts +14 -0
  45. package/templates/blank-intl/supabase/migrations/00000000000000_smoodly.sql +518 -0
  46. package/templates/blank-intl.tsconfig.json +28 -0
  47. package/templates/blank.tsconfig.json +28 -0
  48. package/templates/lorem/AGENTS.md +50 -0
  49. package/templates/lorem/CLAUDE.md +1 -0
  50. package/templates/{next → lorem}/README.md +5 -4
  51. package/templates/lorem/_gitignore +5 -0
  52. package/templates/lorem/app/(site)/[...path]/page.tsx +15 -0
  53. package/templates/{next/app → lorem/app/(site)}/layout.tsx +11 -11
  54. package/templates/lorem/app/(site)/page.tsx +11 -0
  55. package/templates/lorem/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  56. package/templates/lorem/app/(smoodly)/admin/layout.tsx +1 -0
  57. package/templates/{next → lorem}/app/globals.css +4 -0
  58. package/templates/{next-intl → lorem}/components/elements/Callout.tsx +1 -1
  59. package/templates/{next → lorem}/components/elements/Cta.tsx +1 -1
  60. package/templates/{next → lorem}/components/pages/Article.tsx +3 -2
  61. package/templates/{next → lorem}/components/pages/Articles.tsx +7 -7
  62. package/templates/{next-intl → lorem}/components/pages/Page.tsx +11 -1
  63. package/templates/{next → lorem}/components/sections/ImageText.tsx +1 -1
  64. package/templates/{next → lorem}/components/sections/Points.tsx +1 -1
  65. package/templates/lorem/next.config.ts +8 -0
  66. package/templates/lorem/package.json +27 -0
  67. package/templates/{next-intl → lorem}/scripts/seed.ts +30 -5
  68. package/templates/lorem/smoodly/admin.tsx +6 -0
  69. package/templates/lorem/smoodly/ops.ts +11 -0
  70. package/templates/lorem/smoodly/richtext.tsx +7 -0
  71. package/templates/lorem/smoodly/server.ts +96 -0
  72. package/templates/lorem/tsconfig.json +41 -0
  73. package/templates/lorem-intl/AGENTS.md +50 -0
  74. package/templates/lorem-intl/CLAUDE.md +1 -0
  75. package/templates/{next-intl → lorem-intl}/README.md +2 -1
  76. package/templates/lorem-intl/_gitignore +5 -0
  77. package/templates/lorem-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  78. package/templates/lorem-intl/app/(smoodly)/admin/layout.tsx +15 -0
  79. package/templates/lorem-intl/app/[locale]/[...path]/page.tsx +18 -0
  80. package/templates/{next-intl → lorem-intl}/app/[locale]/layout.tsx +5 -6
  81. package/templates/lorem-intl/app/[locale]/page.tsx +15 -0
  82. package/templates/{next-intl → lorem-intl}/app/globals.css +4 -0
  83. package/templates/{next → lorem-intl}/components/elements/Callout.tsx +1 -1
  84. package/templates/{next-intl → lorem-intl}/components/elements/Cta.tsx +1 -1
  85. package/templates/{next-intl → lorem-intl}/components/pages/Article.tsx +3 -2
  86. package/templates/{next-intl → lorem-intl}/components/pages/Articles.tsx +7 -7
  87. package/templates/{next → lorem-intl}/components/pages/Page.tsx +11 -1
  88. package/templates/{next-intl → lorem-intl}/components/sections/ImageText.tsx +1 -1
  89. package/templates/{next-intl → lorem-intl}/components/sections/Points.tsx +1 -1
  90. package/templates/lorem-intl/i18n/navigation.ts +6 -0
  91. package/templates/lorem-intl/i18n/request.ts +24 -0
  92. package/templates/lorem-intl/i18n/routing.ts +17 -0
  93. package/templates/lorem-intl/next.config.ts +11 -0
  94. package/templates/lorem-intl/package.json +28 -0
  95. package/templates/lorem-intl/proxy.ts +11 -0
  96. package/templates/{next → lorem-intl}/scripts/seed.ts +30 -5
  97. package/templates/lorem-intl/smoodly/admin.tsx +6 -0
  98. package/templates/lorem-intl/smoodly/ops.ts +11 -0
  99. package/templates/lorem-intl/smoodly/richtext.tsx +7 -0
  100. package/templates/lorem-intl/smoodly/server.ts +97 -0
  101. package/templates/lorem-intl/tsconfig.json +41 -0
  102. package/templates/lorem-intl.tsconfig.json +28 -0
  103. package/templates/lorem.tsconfig.json +28 -0
  104. /package/templates/{next → blank}/_gitignore +0 -0
  105. /package/templates/{next/app → blank/app/(site)}/page.tsx +0 -0
  106. /package/templates/{next → blank}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  107. /package/templates/{next → blank}/app/(smoodly)/admin/layout.tsx +0 -0
  108. /package/templates/{next → blank}/next.config.ts +0 -0
  109. /package/templates/{next → blank}/smoodly/admin.tsx +0 -0
  110. /package/templates/{next → blank}/smoodly/ops.ts +0 -0
  111. /package/templates/{next → blank}/smoodly/richtext.tsx +0 -0
  112. /package/templates/{next → blank}/tsconfig.json +0 -0
  113. /package/templates/{next-intl → blank-intl}/_gitignore +0 -0
  114. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/[[...segments]]/page.tsx +0 -0
  115. /package/templates/{next-intl → blank-intl}/app/(smoodly)/admin/layout.tsx +0 -0
  116. /package/templates/{next-intl → blank-intl}/app/[locale]/[...path]/page.tsx +0 -0
  117. /package/templates/{next-intl → blank-intl}/app/[locale]/page.tsx +0 -0
  118. /package/templates/{next-intl → blank-intl}/i18n/navigation.ts +0 -0
  119. /package/templates/{next-intl → blank-intl}/i18n/request.ts +0 -0
  120. /package/templates/{next-intl → blank-intl}/i18n/routing.ts +0 -0
  121. /package/templates/{next-intl → blank-intl}/next.config.ts +0 -0
  122. /package/templates/{next-intl → blank-intl}/proxy.ts +0 -0
  123. /package/templates/{next-intl → blank-intl}/smoodly/admin.tsx +0 -0
  124. /package/templates/{next-intl → blank-intl}/smoodly/ops.ts +0 -0
  125. /package/templates/{next-intl → blank-intl}/smoodly/richtext.tsx +0 -0
  126. /package/templates/{next-intl → blank-intl}/tsconfig.json +0 -0
  127. /package/templates/{next → lorem}/components/ArticleCard.tsx +0 -0
  128. /package/templates/{next → lorem}/components/articles.ts +0 -0
  129. /package/templates/{next → lorem}/components/pages/Home.tsx +0 -0
  130. /package/templates/{next → lorem}/components/sections/FavouriteArticles.tsx +0 -0
  131. /package/templates/{next → lorem}/components/sections/Footer.tsx +0 -0
  132. /package/templates/{next → lorem}/components/sections/Header.tsx +0 -0
  133. /package/templates/{next → lorem}/components/sections/Hero.tsx +0 -0
  134. /package/templates/{next → lorem}/components/sections/PageHero.tsx +0 -0
  135. /package/templates/{next → lorem}/components/sections/People.tsx +0 -0
  136. /package/templates/{next → lorem}/components/sections/Quote.tsx +0 -0
  137. /package/templates/{next → lorem}/components/strings.ts +0 -0
  138. /package/templates/{next → lorem}/messages/en.json +0 -0
  139. /package/templates/{next → lorem}/public/images/about_main.jpg +0 -0
  140. /package/templates/{next → lorem}/public/images/article_photo.jpg +0 -0
  141. /package/templates/{next → lorem}/public/images/hero_image.jpg +0 -0
  142. /package/templates/{next → lorem}/public/images/home_section_2.jpg +0 -0
  143. /package/templates/{next → lorem}/smoodly/collections.ts +0 -0
  144. /package/templates/{next → lorem}/smoodly/config.ts +0 -0
  145. /package/templates/{next → lorem}/smoodly/elements.ts +0 -0
  146. /package/templates/{next → lorem}/smoodly/sections.ts +0 -0
  147. /package/templates/{next → lorem}/smoodly/shared.ts +0 -0
  148. /package/templates/{next → lorem}/smoodly/toolsets.ts +0 -0
  149. /package/templates/{next → lorem}/supabase/migrations/00000000000000_smoodly.sql +0 -0
  150. /package/templates/{next-intl → lorem-intl}/components/ArticleCard.tsx +0 -0
  151. /package/templates/{next-intl → lorem-intl}/components/articles.ts +0 -0
  152. /package/templates/{next-intl → lorem-intl}/components/pages/Home.tsx +0 -0
  153. /package/templates/{next-intl → lorem-intl}/components/sections/FavouriteArticles.tsx +0 -0
  154. /package/templates/{next-intl → lorem-intl}/components/sections/Footer.tsx +0 -0
  155. /package/templates/{next-intl → lorem-intl}/components/sections/Header.tsx +0 -0
  156. /package/templates/{next-intl → lorem-intl}/components/sections/Hero.tsx +0 -0
  157. /package/templates/{next-intl → lorem-intl}/components/sections/PageHero.tsx +0 -0
  158. /package/templates/{next-intl → lorem-intl}/components/sections/People.tsx +0 -0
  159. /package/templates/{next-intl → lorem-intl}/components/sections/Quote.tsx +0 -0
  160. /package/templates/{next-intl → lorem-intl}/components/strings.ts +0 -0
  161. /package/templates/{next-intl → lorem-intl}/messages/en.json +0 -0
  162. /package/templates/{next-intl → lorem-intl}/messages/fi.json +0 -0
  163. /package/templates/{next-intl → lorem-intl}/public/images/about_main.jpg +0 -0
  164. /package/templates/{next-intl → lorem-intl}/public/images/article_photo.jpg +0 -0
  165. /package/templates/{next-intl → lorem-intl}/public/images/hero_image.jpg +0 -0
  166. /package/templates/{next-intl → lorem-intl}/public/images/home_section_2.jpg +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
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from "react";
2
+ import { notFound } from "next/navigation";
3
+ import { hasLocale, NextIntlClientProvider } from "next-intl";
4
+ import { routing } from "@/i18n/routing";
5
+ import { href, q } from "@/smoodly/server";
6
+ import "../globals.css";
7
+
8
+ // The site's root layout under the locale segment; the admin has its own
9
+ // under app/(smoodly)/admin, outside [locale], as a second root layout.
10
+ // The `site` shared item gives the document title and the footer line,
11
+ // read per locale through `q`, null until published in this locale. The
12
+ // locale links are the visitor's only language switch, since locale
13
+ // detection is off (see i18n/routing.ts).
14
+ export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
15
+ const { locale } = await params;
16
+ const site = await q().getShared("site", { locale });
17
+ return { title: site?.name ?? "Smoodly site" };
18
+ }
19
+
20
+ export default async function LocaleLayout({ children, params }: { children: ReactNode; params: Promise<{ locale: string }> }) {
21
+ const { locale } = await params;
22
+ if (!hasLocale(routing.locales, locale)) notFound();
23
+ const site = await q().getShared("site", { locale });
24
+ const locales = routing.locales.map((code) => ({ code, url: href("/", code), current: code === locale }));
25
+ return (
26
+ <html lang={locale}>
27
+ <body>
28
+ <NextIntlClientProvider>
29
+ {locales.length > 1 && (
30
+ <nav className="container locales" aria-label="Language">
31
+ {locales.map((l) => (
32
+ <a key={l.code} href={l.url} aria-current={l.current ? "page" : undefined}>{l.code}</a>
33
+ ))}
34
+ </nav>
35
+ )}
36
+ <main>{children}</main>
37
+ {site && <footer className="container site-footer">{site.name} · {site.tagline}</footer>}
38
+ </NextIntlClientProvider>
39
+ </body>
40
+ </html>
41
+ );
42
+ }
@@ -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);
@@ -0,0 +1 @@
1
+ {}
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.13",
17
+ "smoodly": "0.0.15",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -0,0 +1,72 @@
1
+ // The starter's content, written through the stores (never raw inserts:
2
+ // the refs index and publish integrity are part of the point). Ships in
3
+ // the scaffold; create-smoodly-app runs `npm run seed` after the first
4
+ // editor. Idempotent: exits once the home page exists in the default
5
+ // locale. Writes the same copy into every configured locale, whatever
6
+ // their codes, so every locale's "/" resolves.
7
+ // Run: npm run seed (needs .env.local and a Supabase stack).
8
+ import next from "@next/env";
9
+ import type { PageTree } from "smoodly";
10
+ const { loadEnvConfig } = next;
11
+
12
+ loadEnvConfig(process.cwd());
13
+
14
+ const { config } = await import("../smoodly/config");
15
+ const { stores } = await import("../smoodly/server");
16
+ const { pages, entries } = stores();
17
+
18
+ const DEFAULT = config.locales.default;
19
+ const OTHERS = config.locales.supported.filter((l) => l !== DEFAULT);
20
+
21
+ if (await pages.resolvePath(DEFAULT, "/")) {
22
+ console.log(`Already seeded — "home" exists in "${DEFAULT}".`);
23
+ process.exit(0);
24
+ }
25
+
26
+ // Richtext is TipTap JSON, the stored format; RichText renders it.
27
+ type Node = Record<string, unknown>;
28
+ const p = (s: string): Node => ({ type: "paragraph", content: [{ type: "text", text: s }] });
29
+ const button = (label: string, href: string): Node => ({ type: "element", attrs: { name: "button", fields: { label, href }, styles: {} } });
30
+ const doc = (...content: Node[]) => ({ type: "doc", content });
31
+
32
+ // --- the site item: name and tagline, published in every locale ------
33
+
34
+ const site = await entries.create("site", { name: "My site", tagline: "Built with Smoodly" }, { locale: DEFAULT });
35
+ await entries.setStatus("site", site.id, DEFAULT, "published");
36
+ for (const locale of OTHERS) {
37
+ await entries.addLocale("site", site.id, locale, { from: DEFAULT });
38
+ await entries.setStatus("site", site.id, locale, "published");
39
+ }
40
+
41
+ // --- the home page: one Text section ---------------------------------
42
+
43
+ const tree = {
44
+ meta: { description: "A new site, built with Smoodly." },
45
+ zones: {
46
+ body: [
47
+ {
48
+ id: "node-hello",
49
+ type: "text",
50
+ fields: {
51
+ title: "Hello",
52
+ body: doc(
53
+ p("This is your home page. Open the admin to edit this text, add sections, create pages and write posts."),
54
+ p("Everything you see is defined in components/ and registered in smoodly/. The README says where things are."),
55
+ button("Open the admin", "/admin"),
56
+ ),
57
+ },
58
+ },
59
+ ],
60
+ },
61
+ } as unknown as PageTree;
62
+
63
+ const home = await pages.createPage({ locale: DEFAULT, slug: "home", title: "Home", template: "home" });
64
+ await pages.saveDraft(home.id, DEFAULT, tree);
65
+ await pages.publish(home.id, DEFAULT);
66
+ for (const locale of OTHERS) {
67
+ await pages.addLocale(home.id, locale, { from: DEFAULT, title: "Home" });
68
+ await pages.saveDraft(home.id, locale, tree);
69
+ await pages.publish(home.id, locale);
70
+ }
71
+
72
+ console.log(`Seeded the blank starter in "${[DEFAULT, ...OTHERS].join('", "')}": the home page and the site item, published.`);
@@ -0,0 +1,26 @@
1
+ import { f, smoodly } from "smoodly";
2
+ import { text } from "./toolsets";
3
+
4
+ // A collection: structured content with its own admin list, served at
5
+ // `path` by its entry page (components/pages/Post.tsx). Fields marked
6
+ // .localized() are stored per locale; the rest are shared across
7
+ // locales. `versions: true` keeps a history per locale. To add a
8
+ // collection: copy this block, add it to `collections`, write its view
9
+ // into a new migration with `collectionSQL` from "smoodly", and apply
10
+ // it (supabase db push, or supabase db reset locally).
11
+ export const posts = smoodly.collection({
12
+ name: "posts",
13
+ title: "Posts",
14
+ titleField: "title",
15
+ path: "posts",
16
+ order: { by: "date", direction: "desc" },
17
+ versions: true,
18
+ fields: {
19
+ title: f.text().localized(),
20
+ slug: f.slug({ from: "title" }),
21
+ date: f.date(),
22
+ body: f.richtext({ toolset: text }).localized(),
23
+ },
24
+ });
25
+
26
+ export const collections = [posts];
@@ -0,0 +1,38 @@
1
+ import { defineConfig, f } from "smoodly";
2
+ import { routing } from "@/i18n/routing";
3
+ import { collections } from "./collections";
4
+ import { sections } from "./sections";
5
+ import { shared } from "./shared";
6
+ import { elements } from "./elements";
7
+ import { HomePage } from "@/components/pages/Home";
8
+ import { StandardPage } from "@/components/pages/Page";
9
+ import { PostPage } from "@/components/pages/Post";
10
+
11
+ export { collections, sections, shared, elements };
12
+
13
+ // The registry and the plain settings, one file. Runtime-free: nothing
14
+ // here imports a route file or Next, so a script and the CLI's template
15
+ // build can load it outside the app. The locale set comes from
16
+ // i18n/routing.ts (plain config, no Next inside) — the proxy runs in the
17
+ // edge runtime and could not import this file the other way round.
18
+ export const config = defineConfig({
19
+ registry: {
20
+ sections,
21
+ elements,
22
+ collections,
23
+ shared,
24
+ pages: [HomePage, StandardPage, PostPage],
25
+ },
26
+ locales: { default: routing.defaultLocale, supported: [...routing.locales] },
27
+ // The record the site root serves.
28
+ homePageSlug: "home",
29
+ // Editors can nest pages three deep.
30
+ pages: { tree: { depth: 3 } },
31
+ // Every section gets this select beside its own styles; the section
32
+ // wrapper emits it as data-tone, and app/globals.css styles it.
33
+ styles: {
34
+ sections: {
35
+ tone: f.select(["paper", "ink"]).default("paper"),
36
+ },
37
+ },
38
+ });
@@ -0,0 +1,5 @@
1
+ // The site's element registrations, in one list: the config registers
2
+ // them, toolsets embed them by value, and smoodly/richtext.tsx renders them.
3
+ import { Button } from "@/components/elements/Button";
4
+
5
+ export const elements = [Button];
@@ -0,0 +1,5 @@
1
+ // The site's section registrations, in one list. config.ts puts it in
2
+ // the registry; the stores and the renderer share the same array.
3
+ import { Text } from "@/components/sections/Text";
4
+
5
+ export const sections = [Text];
@@ -4,22 +4,23 @@
4
4
  // secret is a service-role key or a cloud write key is invisible here.
5
5
  //
6
6
  // Lazy on purpose, the site and renderer included: config.ts imports the
7
- // page shapes, and a page shape may import this file to fetch through
8
- // the site (components/pages/Articles.tsx). That is a cycle, and the
7
+ // page shapes, and a page shape may import this file to read through
8
+ // `q` (components/pages/Articles.tsx). That is a cycle, and the
9
9
  // admin route enters it from config.ts — so nothing here may read the
10
10
  // config at module scope.
11
11
  import {
12
12
  createSmoodlyClient, createSmoodlySite, smoodlyEnv, SupabaseAssetStore, SupabaseEntryStore, SupabasePageStore, SupabasePathIndex, type SmoodlySite,
13
13
  } from "smoodly";
14
14
  import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin/next";
15
- import { createPageRenderer, type PageRegistration } from "smoodly/next";
15
+ import { createPageRenderer, createSmoodlyQueries, type PageRegistration } from "smoodly/next";
16
16
  import type { SupabaseClient } from "@supabase/supabase-js";
17
17
  import { getPathname } from "@/i18n/navigation";
18
18
  import { collections, config, elements, sections, shared } from "./config";
19
19
 
20
20
  type Renderer = ReturnType<typeof createPageRenderer>;
21
+ type Queries = ReturnType<typeof createSmoodlyQueries<typeof collections, typeof shared>>;
21
22
  type Bound = {
22
- db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; assets: SupabaseAssetStore; auth: AdminAuth; site: SmoodlySite; renderer: Renderer;
23
+ db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; assets: SupabaseAssetStore; auth: AdminAuth; site: SmoodlySite; renderer: Renderer; q: Queries;
23
24
  };
24
25
 
25
26
  let cached: Bound | null = null;
@@ -44,17 +45,18 @@ function bound(): Bound {
44
45
  // takes this local closure, not the export, so it never re-enters `bound()`.
45
46
  const stores = () => ({ pages, entries, assets });
46
47
  // The site layer: (locale, path) → page or entry, and the content API
47
- // (site.getPagesTree for menus, site.getEntries for lists).
48
- // Shadows the exported `site` below on purpose: `createPageRenderer`
49
- // takes this local value, not the export, so it never re-enters `bound()`.
50
- const site = createSmoodlySite({ stores, collections, shared, sections, elements, locales, homePageSlug: config.homePageSlug });
48
+ // `q` wraps below. Not exported — the app reads through `q`, which adds
49
+ // draft awareness and the cache.
50
+ const site = createSmoodlySite({ stores, collections, shared, sections, elements, locales, homePageSlug: config.homePageSlug, pages: config.registry.pages ?? [] });
51
51
  const renderer = createPageRenderer({
52
52
  site,
53
53
  sections,
54
54
  pages: (config.registry.pages ?? []) as PageRegistration[],
55
55
  href,
56
56
  });
57
- cached = { db, pages, entries, assets, auth, site, renderer };
57
+ // The read API (spec 2026-09-11): typed by the registry, draft-aware, cached under tags the admin expires.
58
+ const q = createSmoodlyQueries({ site, collections, shared });
59
+ cached = { db, pages, entries, assets, auth, site, renderer, q };
58
60
  return cached;
59
61
  }
60
62
 
@@ -81,13 +83,15 @@ export function publicAuth(): AuthConfig {
81
83
  * becomes getPathname, which adds the locale prefix. */
82
84
  export const href = (path: string, locale: string) => getPathname({ locale, href: path });
83
85
 
84
- /** The content API for page views that fetch (the Articles index, an
85
- * article's related list): `site().getEntries("articles", { locale })`. */
86
- export const site = (): SmoodlySite => bound().site;
86
+ /** The read API (spec 2026-09-11) for layouts and page views that fetch:
87
+ * `q().getEntries("articles", { locale, limit: 3 })`, `q().getShared("header",
88
+ * { locale })`, `q().getPageTree({ locale })`. Typed by the registry, the
89
+ * live draft in the editor preview, cached under tags the admin expires.
90
+ * Rename it if `q` is not to your taste — this file is yours. */
91
+ export const q = (): Queries => bound().q;
87
92
 
88
93
  /** The two route files call these. */
89
94
  export const renderSmoodlyPath: Renderer["renderSmoodlyPath"] = (...args) => bound().renderer.renderSmoodlyPath(...args);
90
95
  export const smoodlyMetadata: Renderer["smoodlyMetadata"] = (...args) => bound().renderer.smoodlyMetadata(...args);
91
96
  export const renderSmoodlyPage: Renderer["renderSmoodlyPage"] = (...args) => bound().renderer.renderSmoodlyPage(...args);
92
97
  export const smoodlyPageMetadata: Renderer["smoodlyPageMetadata"] = (...args) => bound().renderer.smoodlyPageMetadata(...args);
93
- export const getSmoodlyShared: Renderer["getSmoodlyShared"] = (...args) => bound().renderer.getSmoodlyShared(...args);
@@ -0,0 +1,16 @@
1
+ // Shared content: code-declared singletons, edited once under Globals
2
+ // and read anywhere. `site` is an object item — fields code consumes;
3
+ // the layout reads it through q().getShared("site"). A visual item
4
+ // (`section: SomeSection`) renders through a section instead.
5
+ import { f, smoodly } from "smoodly";
6
+
7
+ export const site = smoodly.shared({
8
+ name: "site",
9
+ title: "Site",
10
+ fields: {
11
+ name: f.text(),
12
+ tagline: f.text().localized(),
13
+ },
14
+ });
15
+
16
+ export const shared = [site];
@@ -0,0 +1,14 @@
1
+ // Toolsets are values, not registrations: a richtext field names one.
2
+ // `text` is the starter's one set — the default marks and block nodes,
3
+ // plus the Button element. Elements are embedded by value; the config
4
+ // must also register them (smoodly/elements.ts; defineConfig checks).
5
+ // Relative imports on purpose: collections.ts pulls this file into
6
+ // create-smoodly-app's template build, which runs outside the app.
7
+ import { smoodly } from "smoodly";
8
+ import { Button } from "../components/elements/Button";
9
+
10
+ export const text = smoodly.toolset({
11
+ marks: ["bold", "italic", "link"],
12
+ nodes: ["h2", "h3", "bulletList", "orderedList", "blockquote"],
13
+ elements: [Button],
14
+ });