create-smoodly-app 0.0.6 → 0.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "type": "module",
12
12
  "bin": {
13
- "create-smoodly-app": "./dist/index.js"
13
+ "create-smoodly-app": "dist/index.js"
14
14
  },
15
15
  "files": [
16
16
  "dist",
@@ -1,11 +1,27 @@
1
1
  import type { ReactNode } from "react";
2
+ import { Footer } from "@/components/sections/Footer";
3
+ import { contact, footer } from "@/smoodly/shared";
4
+ import { getSmoodlyShared } from "@/smoodly/server";
2
5
 
3
6
  export const metadata = { title: "Smoodly site" };
4
7
 
5
- export default function RootLayout({ children }: { children: ReactNode }) {
8
+ // The site's root layout. The admin has its own under app/(smoodly)/admin
9
+ // two root layouts, a full page load between them, which is fine: they
10
+ // are separate apps in spirit. The footer and the contact block are
11
+ // shared content: fetched here, cached under their tag, null until
12
+ // published.
13
+ export default async function RootLayout({ children }: { children: ReactNode }) {
14
+ const [footerProps, contactFields] = await Promise.all([
15
+ getSmoodlyShared(footer),
16
+ getSmoodlyShared(contact),
17
+ ]);
6
18
  return (
7
19
  <html lang="en">
8
- <body>{children}</body>
20
+ <body>
21
+ {children}
22
+ {contactFields && <address>{contactFields.email} · {contactFields.address}</address>}
23
+ {footerProps && <Footer {...footerProps} />}
24
+ </body>
9
25
  </html>
10
26
  );
11
27
  }
@@ -2,6 +2,7 @@ import { smoodly, z, Zone, type Props } from "smoodly";
2
2
  import { Hero } from "../sections/Hero";
3
3
  import { ArticleList } from "../sections/ArticleList";
4
4
  import { CtaBanner } from "../sections/CtaBanner";
5
+ import { Testimonials } from "../sections/Testimonials";
5
6
 
6
7
  // A page shape: zones declared, <Zone /> placed, one file (DESIGN.md §3).
7
8
  // `slug` makes it fixed — one record the editor can neither delete nor
@@ -12,7 +13,7 @@ export const homeSchema = smoodly.schema.page({
12
13
  slug: "home",
13
14
  zones: {
14
15
  hero: z.sections([Hero]).max(1),
15
- body: z.freeform({ sections: [ArticleList] }),
16
+ body: z.freeform({ sections: [ArticleList, Testimonials] }),
16
17
  // Locked: fields only. One CtaBanner, always there, never movable.
17
18
  prefooter: z.locked(CtaBanner),
18
19
  },
@@ -3,6 +3,7 @@ import { smoodly, z, Zone, type Props } from "smoodly";
3
3
  import { Hero } from "../sections/Hero";
4
4
  import { ArticleList } from "../sections/ArticleList";
5
5
  import { CtaBanner } from "../sections/CtaBanner";
6
+ import { Testimonials } from "../sections/Testimonials";
6
7
 
7
8
  // Open registration: editors create as many of these as they like, at
8
9
  // any depth the config allows. `page` is where this one sits in the tree.
@@ -11,7 +12,7 @@ export const pageSchema = smoodly.schema.page({
11
12
  title: "Page",
12
13
  zones: {
13
14
  hero: z.sections([Hero]).max(1),
14
- body: z.freeform({ sections: [ArticleList] }),
15
+ body: z.freeform({ sections: [ArticleList, Testimonials] }),
15
16
  prefooter: z.locked(CtaBanner),
16
17
  },
17
18
  meta: { seo: true },
@@ -0,0 +1,33 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+
3
+ // The site footer — a VISUAL shared item (smoodly/shared.ts): registered
4
+ // like any section, stored once, rendered by the locale layout through
5
+ // getSmoodlyShared, never placed in a page tree. The tagline is per
6
+ // language; the copyright line is the same everywhere.
7
+ export const footerSchema = smoodly.schema.section({
8
+ name: "footer",
9
+ title: "Footer",
10
+ description: "The site-wide footer.",
11
+ fields: {
12
+ tagline: f.text().localized(),
13
+ copyright: f.text(),
14
+ },
15
+ styles: {
16
+ tone: f.select(["light", "dark"]).default("dark"),
17
+ },
18
+ sample: {
19
+ tagline: "Made with Smoodly",
20
+ copyright: "© Laniakea Studio",
21
+ },
22
+ });
23
+
24
+ function FooterView({ tagline, copyright, styles }: Props<typeof footerSchema>) {
25
+ return (
26
+ <smoodly.Section as="footer" styles={styles}>
27
+ <p>{tagline}</p>
28
+ <small>{copyright}</small>
29
+ </smoodly.Section>
30
+ );
31
+ }
32
+
33
+ export const Footer = smoodly.section(footerSchema, FooterView);
@@ -0,0 +1,30 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+
3
+ // A testimonials band — placed on pages from the "+" picker's Shared
4
+ // group as a LINKED node (spec 2026-09-07 §4): one row, edited under
5
+ // Shared content, rendered wherever it is placed.
6
+ export const testimonialsSchema = smoodly.schema.section({
7
+ name: "testimonials",
8
+ title: "Testimonials",
9
+ description: "What people say about us.",
10
+ fields: {
11
+ heading: f.text().localized(),
12
+ quotes: f.richtext().localized(),
13
+ },
14
+ styles: {
15
+ tone: f.select(["light", "dark"]).default("light"),
16
+ },
17
+ sample: {
18
+ heading: "Kind words",
19
+ },
20
+ });
21
+
22
+ function TestimonialsView({ heading, styles }: Props<typeof testimonialsSchema>) {
23
+ return (
24
+ <smoodly.Section styles={styles}>
25
+ <h2>{heading}</h2>
26
+ </smoodly.Section>
27
+ );
28
+ }
29
+
30
+ export const Testimonials = smoodly.section(testimonialsSchema, TestimonialsView);
@@ -13,7 +13,7 @@
13
13
  "next": "^16.3.4",
14
14
  "react": "^19",
15
15
  "react-dom": "^19",
16
- "smoodly": "0.0.6"
16
+ "smoodly": "0.0.8"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^26.4.1",
@@ -1,24 +1,31 @@
1
1
  import { f, smoodly } from "smoodly";
2
2
 
3
+ // A person's name is the same in every language; the role is translated.
4
+ // Shared fields live on the entry, .localized() ones on its locale rows
5
+ // (spec 2026-09-06 §7).
3
6
  export const people = smoodly.collection({
4
7
  name: "people",
5
8
  title: "People",
6
9
  titleField: "name",
7
10
  fields: {
8
11
  name: f.text(),
9
- role: f.text().optional(),
12
+ role: f.text().optional().localized(),
10
13
  },
11
14
  });
12
15
 
16
+ // Articles keep a version history per locale (`versions: true`): every
17
+ // save is a snapshot, publish moves a pointer, and the published site
18
+ // keeps serving the published snapshot while a draft moves on.
13
19
  export const articles = smoodly.collection({
14
20
  name: "articles",
15
21
  title: "Articles",
16
22
  titleField: "title",
17
23
  path: "posts",
24
+ versions: true,
18
25
  fields: {
19
- title: f.text(),
26
+ title: f.text().localized(),
20
27
  slug: f.slug({ from: "title" }),
21
- excerpt: f.text().optional(),
28
+ excerpt: f.text().optional().localized(),
22
29
  author: f.ref(() => people),
23
30
  },
24
31
  });
@@ -1,11 +1,12 @@
1
1
  import { defineConfig, f } from "smoodly";
2
2
  import { collections } from "./collections";
3
3
  import { sections } from "./sections";
4
+ import { shared } from "./shared";
4
5
  import { HomePage } from "@/components/pages/Home";
5
6
  import { StandardPage } from "@/components/pages/Page";
6
7
  import { ArticlePage } from "@/components/pages/Article";
7
8
 
8
- export { collections, sections };
9
+ export { collections, sections, shared };
9
10
 
10
11
  // The registry and the plain settings, one file. Runtime-free: nothing
11
12
  // here imports a route file or Next, so a script, the CLI's template
@@ -14,6 +15,7 @@ export const config = defineConfig({
14
15
  registry: {
15
16
  sections,
16
17
  collections,
18
+ shared,
17
19
  pages: [HomePage, StandardPage, ArticlePage],
18
20
  },
19
21
  locales: { default: "en", supported: ["en"] },
@@ -4,5 +4,7 @@
4
4
  import { Hero } from "@/components/sections/Hero";
5
5
  import { ArticleList } from "@/components/sections/ArticleList";
6
6
  import { CtaBanner } from "@/components/sections/CtaBanner";
7
+ import { Footer } from "@/components/sections/Footer";
8
+ import { Testimonials } from "@/components/sections/Testimonials";
7
9
 
8
- export const sections = [Hero, ArticleList, CtaBanner];
10
+ export const sections = [Hero, ArticleList, CtaBanner, Footer, Testimonials];
@@ -8,7 +8,7 @@ import {
8
8
  import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin/next";
9
9
  import { createPageRenderer, type PageRegistration } from "smoodly/next";
10
10
  import type { SupabaseClient } from "@supabase/supabase-js";
11
- import { collections, config, sections } from "./config";
11
+ import { collections, config, sections, shared } from "./config";
12
12
 
13
13
  let cached: { db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; auth: AdminAuth } | null = null;
14
14
 
@@ -19,7 +19,7 @@ function bound() {
19
19
  // ONE path index for both stores: pages and entries share the URL space.
20
20
  const paths = new SupabasePathIndex(db);
21
21
  const locales = config.locales;
22
- const entries = new SupabaseEntryStore(db, collections, { paths, locales });
22
+ const entries = new SupabaseEntryStore(db, collections, { paths, locales, shared });
23
23
  const pages = new SupabasePageStore(db, {
24
24
  sections, entries, paths, locales,
25
25
  homePageSlug: config.homePageSlug,
@@ -58,13 +58,14 @@ export const href = (path: string, _locale: string) => path;
58
58
  export const site = createSmoodlySite({
59
59
  stores,
60
60
  collections,
61
+ shared,
61
62
  sections,
62
63
  locales: config.locales,
63
64
  homePageSlug: config.homePageSlug,
64
65
  });
65
66
 
66
67
  /** The two route files call these. */
67
- export const { renderSmoodlyPath, smoodlyMetadata, renderSmoodlyPage, smoodlyPageMetadata } = createPageRenderer({
68
+ export const { renderSmoodlyPath, smoodlyMetadata, renderSmoodlyPage, smoodlyPageMetadata, getSmoodlyShared } = createPageRenderer({
68
69
  site,
69
70
  sections,
70
71
  pages: (config.registry.pages ?? []) as PageRegistration[],
@@ -0,0 +1,22 @@
1
+ // Shared content (spec 2026-09-07): code-declared singletons stored as
2
+ // entries. `footer` is visual — the layout renders it; `contact` is an
3
+ // object the layout reads; `testimonials` is visual and placed on pages.
4
+ import { f, smoodly } from "smoodly";
5
+ import { Footer } from "@/components/sections/Footer";
6
+ import { Testimonials } from "@/components/sections/Testimonials";
7
+
8
+ export const footer = smoodly.shared({ name: "footer", title: "Footer", section: Footer, versions: true });
9
+
10
+ export const contact = smoodly.shared({
11
+ name: "contact",
12
+ title: "Contact details",
13
+ fields: {
14
+ email: f.text(),
15
+ phone: f.text().optional(),
16
+ address: f.text().localized(),
17
+ },
18
+ });
19
+
20
+ export const testimonials = smoodly.shared({ name: "testimonials", title: "Testimonials", section: Testimonials });
21
+
22
+ export const shared = [footer, contact, testimonials];