smoodly 0.0.60 → 0.0.62
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/README.md +3 -1
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
- package/dist/admin/editor/CanvasOverlay.js +3 -3
- package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
- package/dist/admin/editor/GlobalCanvas.js +159 -0
- package/dist/admin/editor/InsertPicker.js +1 -1
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/serialize.d.ts +2 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.js +5 -5
- package/dist/admin/shell/GlobalForm.js +80 -7
- package/dist/admin/shell/GlobalList.js +1 -1
- package/dist/admin/shell/{IconRail.d.ts → NavBar.d.ts} +3 -3
- package/dist/admin/shell/NavBar.js +120 -0
- package/dist/admin/shell/global-canvas.d.ts +40 -0
- package/dist/admin/shell/global-canvas.js +60 -0
- package/dist/admin/shell/health-list.js +1 -1
- package/dist/admin/ui/primitives.js +1 -1
- package/dist/admin/ui/theme.js +99 -91
- package/dist/api-client.js +5 -3
- package/dist/astro/index.js +3 -3
- package/dist/cli/push.js +2 -3
- package/dist/env.js +3 -2
- package/dist/push.d.ts +7 -0
- package/dist/push.js +8 -0
- package/dist/space-kind.d.ts +2 -0
- package/dist/space-kind.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/collections/defining.md +57 -0
- package/docs/collections/entry-pages.md +168 -0
- package/docs/collections/ordering.md +47 -0
- package/docs/collections/queries.md +173 -0
- package/docs/content-modeling/changing-schemas.md +79 -0
- package/docs/content-modeling/custom-section.md +94 -0
- package/docs/content-modeling/marks.md +97 -0
- package/docs/content-modeling/overview.md +55 -0
- package/docs/content-modeling/page-types.md +237 -0
- package/docs/content-modeling/sections.md +264 -0
- package/docs/content-modeling/styles-and-settings.md +184 -0
- package/docs/content-modeling/zones.md +80 -0
- package/docs/fields/choices.md +42 -0
- package/docs/fields/conditional.md +72 -0
- package/docs/fields/link.md +31 -0
- package/docs/fields/list-and-object.md +58 -0
- package/docs/fields/media.md +103 -0
- package/docs/fields/overview.md +56 -0
- package/docs/fields/reference-and-tags.md +62 -0
- package/docs/fields/richtext.md +168 -0
- package/docs/fields/text.md +152 -0
- package/docs/frameworks/astro.md +110 -0
- package/docs/frameworks/nextjs.md +147 -0
- package/docs/getting-started/content-from-code.md +263 -0
- package/docs/getting-started/deploying.md +131 -0
- package/docs/getting-started/export-and-import.md +73 -0
- package/docs/getting-started/first-page.md +98 -0
- package/docs/getting-started/install.md +108 -0
- package/docs/getting-started/keys.md +40 -0
- package/docs/getting-started/project-structure.md +142 -0
- package/docs/getting-started/working-together.md +42 -0
- package/docs/globals/defining.md +99 -0
- package/docs/globals/header-and-footer.md +117 -0
- package/docs/index.md +83 -0
- package/docs/languages/language-switcher.md +103 -0
- package/docs/languages/locales.md +119 -0
- package/docs/languages/localized-fields.md +61 -0
- package/docs/media/images-and-crops.md +131 -0
- package/docs/media/library.md +40 -0
- package/docs/pages/drafts-and-publishing.md +44 -0
- package/docs/pages/page-tree.md +51 -0
- package/docs/pages/paths-and-slugs.md +74 -0
- package/docs/pages/seo.md +126 -0
- package/docs/pages/version-history.md +23 -0
- package/docs/reference/upgrading.md +312 -0
- package/package.json +4 -3
- package/dist/admin/shell/IconRail.js +0 -109
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Modeling content
|
|
2
|
+
|
|
3
|
+
The five things you register, what each one holds, and where it lives in your project. Read this once; the rest of the group takes them one at a time.
|
|
4
|
+
|
|
5
|
+
## Registration is the model
|
|
6
|
+
|
|
7
|
+
Smoodly has no schema screens. Each schema sits in a file beside the component that renders it, is paired with that view, and is listed once in `smoodly/config.ts`. From that one definition Smoodly derives the editor form, the entry in *Add section*, the TypeScript props and the checks it runs at publish.
|
|
8
|
+
|
|
9
|
+
Change a field in code and the form changes with it. Nothing an editor does changes the code: editors fill fields and arrange sections inside the limits you set.
|
|
10
|
+
|
|
11
|
+
## The five types
|
|
12
|
+
|
|
13
|
+
| Type | Registered with | What it is |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `Page type` | smoodly.page() | A page component. It declares zones and renders them among your own JSX. Editors pick one when they create a page. |
|
|
16
|
+
| `Section` | smoodly.section() | A full-width component editors place in a zone: a hero, a row of cards, a quote. |
|
|
17
|
+
| `Element` | smoodly.element() | A small component editors insert inside richtext: a callout, a call to action. |
|
|
18
|
+
| `Collection` | smoodly.collection() | A content type with a fixed shape and many entries: posts, people. An entry page, smoodly.entryPage(), renders one entry at its own URL. |
|
|
19
|
+
| `Global` | smoodly.global() | Content that exists once and appears on many pages: the header, the footer, contact details. |
|
|
20
|
+
|
|
21
|
+
The custom section is the sixth piece and is built in: rows of richtext columns editors lay out themselves, offered only where a zone allows it.
|
|
22
|
+
|
|
23
|
+
In the Lorem Co. starter that is nine sections, four page types, two entry pages, three collections, four globals and two elements. Page types, sections and elements live in `components/`, two files each: the schema and the view. Collections and globals are lists in `smoodly/collections.ts` and `smoodly/globals.ts`.
|
|
24
|
+
|
|
25
|
+
## How they nest
|
|
26
|
+
|
|
27
|
+
Containment is short and fixed:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Page type → zone → section
|
|
31
|
+
→ custom section → rows → columns of richtext → elements
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A section never holds another section. A section that needs columns builds them in code, and editors who need a free layout get the custom section. That cap is what keeps drag and drop, rendering and the canvas simple.
|
|
35
|
+
|
|
36
|
+
Around the zones, a page type renders whatever JSX you like: navigation, a grid of the newest posts, a form. That JSX shows in the canvas but is not selectable, so it stays yours.
|
|
37
|
+
|
|
38
|
+
## Content, settings and styles
|
|
39
|
+
|
|
40
|
+
Everything editors change is stored per page, in three groups:
|
|
41
|
+
|
|
42
|
+
- **Fields** are content: the heading, the photo, the list of buttons. They can be localized.
|
|
43
|
+
- **Settings** are the options you declare on a section for how it looks, like which side the image sits on. The view alone interprets them.
|
|
44
|
+
- **Styles** are Smoodly's own spacing, width and alignment steps, offered on every section unless you turn them off.
|
|
45
|
+
|
|
46
|
+
Editors see fields on the *Content* tab and the other two on the *Settings* tab.
|
|
47
|
+
|
|
48
|
+
## Reading order
|
|
49
|
+
|
|
50
|
+
1. [Sections](../content-modeling/sections.md): a schema, a view, marks and registration.
|
|
51
|
+
2. [Page types](../content-modeling/page-types.md): zones, fixed and open pages, the page prop.
|
|
52
|
+
3. [Zones and freedom](../content-modeling/zones.md): the three zone policies.
|
|
53
|
+
4. [Settings, styles and CSS](../content-modeling/styles-and-settings.md), [The custom section](../content-modeling/custom-section.md) and [Marks and inline editing](../content-modeling/marks.md).
|
|
54
|
+
|
|
55
|
+
Then [Fields](../fields/overview.md) for every field type, and [Collections](../collections/defining.md) and [Globals](../globals/defining.md) for the two types that live outside pages.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Defining a page type
|
|
2
|
+
|
|
3
|
+
A page type is a page component with zones: the places where editors add sections. This page walks through the Lorem Co. starter's Home and Page types, and what a page type learns about its place in the site.
|
|
4
|
+
|
|
5
|
+
## The schema
|
|
6
|
+
|
|
7
|
+
A page type starts with `smoodly.schema.page()`: a name, a title, and a `zones` map that gives each zone a policy. The schema is the same file in every framework; in an Astro project it lives under `src/components/pages/`.
|
|
8
|
+
|
|
9
|
+
File: `components/pages/Home.schema.ts`
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { smoodly, z } from "smoodly";
|
|
13
|
+
import { heroSchema } from "../sections/Hero.schema";
|
|
14
|
+
// …
|
|
15
|
+
export const homeSchema = smoodly.schema.page({
|
|
16
|
+
name: "home",
|
|
17
|
+
title: "Home",
|
|
18
|
+
slug: "home",
|
|
19
|
+
zones: {
|
|
20
|
+
hero: z.locked(heroSchema),
|
|
21
|
+
body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
|
|
22
|
+
},
|
|
23
|
+
seo: true,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Name | Description |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `name` | What a page record stores as its type. Keep it stable once pages exist. |
|
|
30
|
+
| `title` | What editors see in the Page type column and when they create a page. |
|
|
31
|
+
| `slug` | Makes the page type fixed: one page, at this URL. Leave it out and editors create as many as they like. See below. |
|
|
32
|
+
| `zones` | One entry per zone, each with a policy from z. Zones and freedom covers the three. |
|
|
33
|
+
| `seo` | `true` gives the page an SEO & Social tab: title, description, social image and a switch to hide it from search engines. |
|
|
34
|
+
| `fields` | The page type's own fields, edited on the page's Settings tab. See below. |
|
|
35
|
+
|
|
36
|
+
## The view
|
|
37
|
+
|
|
38
|
+
**Astro**
|
|
39
|
+
|
|
40
|
+
The view is `src/page-types/home.astro`, named after the schema. It receives `page`, `zones` and `head`. Render each zone with `<Zone>` wherever it belongs, among your own markup:
|
|
41
|
+
|
|
42
|
+
File: `src/page-types/home.astro`
|
|
43
|
+
|
|
44
|
+
```astro
|
|
45
|
+
---
|
|
46
|
+
import { Zone } from "smoodly/astro/components";
|
|
47
|
+
// …
|
|
48
|
+
const { page, zones, head } = Astro.props;
|
|
49
|
+
---
|
|
50
|
+
<Layout head={head} locale={page.locale} locales={page.locales}>
|
|
51
|
+
<Zone of={zones.hero} />
|
|
52
|
+
<Zone of={zones.body} />
|
|
53
|
+
</Layout>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`Layout` is the starter's own component: the document, the site header and footer. Anything you render around the zones shows in the canvas but cannot be selected, so editors cannot move or remove it. `head` is what `Head` writes into the document head: the title, the canonical link and the rest.
|
|
57
|
+
|
|
58
|
+
Register the schema in the `pages` list of `src/smoodly/config.ts`; the view is the file, with nothing else to list:
|
|
59
|
+
|
|
60
|
+
File: `src/smoodly/config.ts`
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
export const config = defineConfig({
|
|
64
|
+
registry: {
|
|
65
|
+
sections,
|
|
66
|
+
// …
|
|
67
|
+
pages: [homeSchema, articlesSchema, pageSchema, newsSchema],
|
|
68
|
+
},
|
|
69
|
+
// …
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Next.js**
|
|
74
|
+
|
|
75
|
+
The view lives beside the schema, in `Home.tsx`. It receives `zones` and `page`. Render each zone with `<Zone>` wherever it belongs, among your own JSX:
|
|
76
|
+
|
|
77
|
+
File: `components/pages/Home.tsx`
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
function HomeView({ zones, page }: Props<typeof homeSchema>) {
|
|
81
|
+
return (
|
|
82
|
+
<PageFrame locale={page.locale} locales={page.locales}>
|
|
83
|
+
<Zone of={zones.hero} />
|
|
84
|
+
<Zone of={zones.body} />
|
|
85
|
+
</PageFrame>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const HomePage = smoodly.page(homeSchema, HomeView);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`PageFrame` is the starter's own component: the site header and a `main` element. Anything you render around the zones shows in the canvas but cannot be selected, so editors cannot move or remove it.
|
|
93
|
+
|
|
94
|
+
Register the schema in the `pages` list of `smoodly/config.ts`, and the component in `pages` in `smoodly/views.ts`:
|
|
95
|
+
|
|
96
|
+
File: `smoodly/config.ts`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
export const config = defineConfig({
|
|
100
|
+
registry: {
|
|
101
|
+
sections,
|
|
102
|
+
// …
|
|
103
|
+
pages: [homeSchema, articlesSchema, pageSchema, newsSchema],
|
|
104
|
+
},
|
|
105
|
+
// …
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
File: `smoodly/views.ts`
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export const views = () =>
|
|
113
|
+
defineViews(config, {
|
|
114
|
+
// …
|
|
115
|
+
pages: [HomePage, ArticlesPage, StandardPage, ArticlePage, NewsPage, NewsPostPage],
|
|
116
|
+
// …
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Fixed and open
|
|
121
|
+
|
|
122
|
+
A page type with a `slug` is **fixed**: it owns exactly one page, at that URL, in every language. Smoodly creates the page the first time the admin loads, so a fresh site has a home page before anyone signs in. Editors can edit it but never create another, rename it, move it or delete it. The slug `home` is special: `homePageSlug` in `defineConfig()` names it, and that page is served at `/`.
|
|
123
|
+
|
|
124
|
+
A fixed slug can differ per language. The starter's Articles index sits at `/posts` in English and `/artikkelit` in Finnish:
|
|
125
|
+
|
|
126
|
+
File: `components/pages/Articles.schema.ts`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
export const articlesSchema = smoodly.schema.page({
|
|
130
|
+
name: "articlesIndex",
|
|
131
|
+
title: "Articles",
|
|
132
|
+
slug: { en: "posts", fi: "artikkelit" },
|
|
133
|
+
zones: {
|
|
134
|
+
hero: z.locked(pageHeroSchema),
|
|
135
|
+
},
|
|
136
|
+
seo: true,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A page type without a `slug` is **open**: editors create as many pages of it as they like, anywhere the page tree allows. When they click *+ New page*, the admin asks which open page type to use. With one open type, as in the starter, it does not ask.
|
|
141
|
+
|
|
142
|
+
Lorem Co. has three fixed types, Home, Articles and News, and one open type, Page. A fixed page is live as soon as its code is deployed, with empty zones until an editor fills them. Its row appears in *Pages* the next time someone opens the admin. Article and News post are [entry pages](../collections/entry-pages.md), which render one entry each.
|
|
143
|
+
|
|
144
|
+
## The page prop
|
|
145
|
+
|
|
146
|
+
`page` tells the view where the page sits, so breadcrumbs and sub-navigation need no query:
|
|
147
|
+
|
|
148
|
+
| Name | Description |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `page.title, page.locale` | The page's title and the language it is rendered in. |
|
|
151
|
+
| `page.path, page.url` | The page's URL within the site, and the same address through your framework's locale routing. On a single-language site they are equal. |
|
|
152
|
+
| `page.ancestors` | Links to the pages above it, root first. |
|
|
153
|
+
| `page.children` | Links to the pages below it. |
|
|
154
|
+
| `page.locales` | One key per language: the page's address there, or null where it does not exist or is unpublished. What a language switcher links to. |
|
|
155
|
+
| `page.fields` | The page type's own fields, typed from the schema. |
|
|
156
|
+
|
|
157
|
+
Each link carries `title`, `path`, `url`, `hasPage` and `fields`. A path in the page tree, which groups pages without being one, has `hasPage` false. The starter's Page type uses both lists:
|
|
158
|
+
|
|
159
|
+
**Astro**
|
|
160
|
+
|
|
161
|
+
File: `src/page-types/page.astro`
|
|
162
|
+
|
|
163
|
+
```astro
|
|
164
|
+
---
|
|
165
|
+
// …
|
|
166
|
+
const shown = page.children.filter((c) => c.hasPage && c.fields.showInNavigation === true);
|
|
167
|
+
---
|
|
168
|
+
<Layout head={head} locale={page.locale} locales={page.locales}>
|
|
169
|
+
{page.ancestors.length > 0 && (
|
|
170
|
+
<nav class="container breadcrumb" aria-label="Breadcrumb">
|
|
171
|
+
{page.ancestors.map((a) => <span>{a.hasPage ? <a href={a.url}>{a.title}</a> : a.title} / </span>)}
|
|
172
|
+
{page.title}
|
|
173
|
+
</nav>
|
|
174
|
+
)}
|
|
175
|
+
{shown.length > 0 && (
|
|
176
|
+
<nav class="container subnav" aria-label="Pages in this section">
|
|
177
|
+
{shown.map((c) => <a href={c.url}>{c.title}</a>)}
|
|
178
|
+
</nav>
|
|
179
|
+
)}
|
|
180
|
+
<Zone of={zones.hero} />
|
|
181
|
+
<Zone of={zones.body} />
|
|
182
|
+
</Layout>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Next.js**
|
|
186
|
+
|
|
187
|
+
File: `components/pages/Page.tsx`
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
function PageView({ zones, page }: Props<typeof pageSchema>) {
|
|
191
|
+
const shown = page.children.filter((c) => c.hasPage && c.fields.showInNavigation === true);
|
|
192
|
+
return (
|
|
193
|
+
<PageFrame locale={page.locale} locales={page.locales}>
|
|
194
|
+
{page.ancestors.length > 0 && (
|
|
195
|
+
<nav className="container breadcrumb" aria-label="Breadcrumb">
|
|
196
|
+
{page.ancestors.map((a) => (
|
|
197
|
+
<span key={a.path}>{a.hasPage ? <Link href={a.url}>{a.title}</Link> : a.title} / </span>
|
|
198
|
+
))}
|
|
199
|
+
{page.title}
|
|
200
|
+
</nav>
|
|
201
|
+
)}
|
|
202
|
+
{shown.length > 0 && (
|
|
203
|
+
<nav className="container subnav" aria-label="Pages in this section">
|
|
204
|
+
{shown.map((c) => <Link key={c.path} href={c.url}>{c.title}</Link>)}
|
|
205
|
+
</nav>
|
|
206
|
+
)}
|
|
207
|
+
<Zone of={zones.hero} />
|
|
208
|
+
<Zone of={zones.body} />
|
|
209
|
+
</PageFrame>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Page fields
|
|
215
|
+
|
|
216
|
+
A page type can declare fields of its own, for values that belong to the page rather than to a section. The starter's Page type has one:
|
|
217
|
+
|
|
218
|
+
File: `components/pages/Page.schema.ts`
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
export const pageSchema = smoodly.schema.page({
|
|
222
|
+
name: "page",
|
|
223
|
+
title: "Page",
|
|
224
|
+
zones: {
|
|
225
|
+
hero: z.locked(pageHeroSchema),
|
|
226
|
+
body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
|
|
227
|
+
},
|
|
228
|
+
seo: true,
|
|
229
|
+
fields: { showInNavigation: f.boolean().default(true) },
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Editors find it on the page's *Settings* tab, next to the title and slug. The view reads it as `page.fields.showInNavigation`, and every link in `ancestors` and `children` carries the linked page's fields, which is how the sub-navigation above filters without a query.
|
|
234
|
+
|
|
235
|
+
Page fields are a settings form, not content: text, link, date, number, boolean, the choice fields, tags, media and objects. Refs, lists and richtext are not allowed, and `.localized()` is not needed because page fields are already stored per language.
|
|
236
|
+
|
|
237
|
+
Next, [Zones and freedom](../content-modeling/zones.md) explains the three policies a zone can have.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Defining a section
|
|
2
|
+
|
|
3
|
+
A section is a full-width component editors place in a zone. You describe its fields once, in a schema file, and Smoodly derives the editor form, the Add section entry and the TypeScript props from that one definition. This page walks through the Lorem Co. starter's Hero.
|
|
4
|
+
|
|
5
|
+
## The schema
|
|
6
|
+
|
|
7
|
+
A section is two files. `Hero.schema.ts` holds the schema: `smoodly.schema.section()` with a name, the title editors see, and a `fields` map built from the `f` helpers. The view, below, renders it. The schema is the same file in every framework; in an Astro project it lives under `src/components/sections/`.
|
|
8
|
+
|
|
9
|
+
File: `components/sections/Hero.schema.ts`
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { f, smoodly } from "smoodly";
|
|
13
|
+
|
|
14
|
+
export const heroSchema = smoodly.schema.section({
|
|
15
|
+
name: "hero",
|
|
16
|
+
title: "Hero",
|
|
17
|
+
description: "The home page opener: heading, text, buttons and a photo.",
|
|
18
|
+
fields: {
|
|
19
|
+
kicker: f.text().optional(),
|
|
20
|
+
heading: f.text(),
|
|
21
|
+
text: f.text(),
|
|
22
|
+
buttons: f.list(f.object({ label: f.text(), href: f.link() })).max(2),
|
|
23
|
+
image: f.image({ aspect: "4:5" }),
|
|
24
|
+
},
|
|
25
|
+
sample: {
|
|
26
|
+
// …
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Name | Description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `name` | What the content stores for every placed Hero, so keep it stable once pages exist. |
|
|
34
|
+
| `title` | The label in Add section and in the inspector. |
|
|
35
|
+
| `description` | One line under the title in Add section. Optional, like icon. |
|
|
36
|
+
| `fields` | What editors fill. Field order is form order: editors see Kicker, Heading, Text, Buttons, Image, top to bottom. |
|
|
37
|
+
| `sample` | What a new Hero starts with. See Sample content below. |
|
|
38
|
+
|
|
39
|
+
Every field is required unless it chains `.optional()`. `.max()` and `.min()` limit a length or a count, `.default()` sets a starting value, and `.localized()` gives a field its own value per language. The full list is on [All fields](../fields/overview.md).
|
|
40
|
+
|
|
41
|
+
## The view
|
|
42
|
+
|
|
43
|
+
**Astro**
|
|
44
|
+
|
|
45
|
+
The view is an `.astro` file named after the schema: `src/sections/hero.astro` renders the section named `hero`. It imports the schema, and `Props<typeof heroSchema>` types `Astro.props`: the fields flat, plus `settings` and `styles`.
|
|
46
|
+
|
|
47
|
+
File: `src/sections/hero.astro`
|
|
48
|
+
|
|
49
|
+
```astro
|
|
50
|
+
---
|
|
51
|
+
import { safeHref, smoodly, type Props as SmoodlyProps } from "smoodly";
|
|
52
|
+
import { Image, Section } from "smoodly/astro/components";
|
|
53
|
+
import { heroSchema } from "@/components/sections/Hero.schema";
|
|
54
|
+
|
|
55
|
+
type Props = SmoodlyProps<typeof heroSchema>;
|
|
56
|
+
const { kicker, heading, text, buttons, image, styles } = Astro.props;
|
|
57
|
+
const m = smoodly.marks(heroSchema);
|
|
58
|
+
---
|
|
59
|
+
<Section styles={styles} class="hero">
|
|
60
|
+
// …
|
|
61
|
+
<h1 class="hero__heading" {...m.field("heading")}>{heading}</h1>
|
|
62
|
+
// …
|
|
63
|
+
{buttons.map((b, i) => {
|
|
64
|
+
const button = m.item("buttons", i);
|
|
65
|
+
return <a class={i === 0 ? "button" : "button button--outline"} href={safeHref(b.href)} {...button.attrs} {...button.field("label")}>{b.label}</a>;
|
|
66
|
+
})}
|
|
67
|
+
// …
|
|
68
|
+
{image && <Image image={image} class="hero__image" sizes="(min-width: 900px) 40vw, 100vw" priority {...m.field("image")} />}
|
|
69
|
+
// …
|
|
70
|
+
</Section>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Three things make this a section rather than a plain component:
|
|
74
|
+
|
|
75
|
+
- **`Section` is the root element.** It comes from `smoodly/astro/components`. Pass it `styles`, and it renders the spacing and width steps an editor picks, plus the attribute the canvas uses to select the section. `as="header"` changes the tag; `class`, `id` and `data-` attributes pass through. The section's own CSS is yours, in `src/styles/global.css` like the starter's.
|
|
76
|
+
- **Marks make fields click-to-edit.** `smoodly.marks(heroSchema)` returns typed helpers. Spread `m.field("heading")` on the element that renders the heading, and clicking it in the canvas opens that field. For a list, `m.item("buttons", i)` gives a scope: spread its `attrs` on the item's root and mark the fields inside with `field()`. `m.object()` does the same for an object field. Outside the admin the marks are plain `data-` attributes. [Marks and inline editing](../content-modeling/marks.md) has the details.
|
|
77
|
+
- **The file name pairs the two.** There is no pairing line and no list of views. The server names a schema with no file, and a file with no schema, at the first request.
|
|
78
|
+
|
|
79
|
+
A link an editor typed goes through `safeHref` from `smoodly`: Astro writes an attribute as given, so a `javascript:` URL would reach the page. `safeHref` passes http(s), mailto, tel and site paths, and gives `null` for anything else, so the link renders with no `href`.
|
|
80
|
+
|
|
81
|
+
**Next.js**
|
|
82
|
+
|
|
83
|
+
The view is an ordinary React component in `Hero.tsx`, beside the schema. It imports the schema, and `Props<typeof heroSchema>` types it: the fields flat, plus `settings` and `styles`.
|
|
84
|
+
|
|
85
|
+
File: `components/sections/Hero.tsx`
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const m = smoodly.marks(heroSchema);
|
|
89
|
+
|
|
90
|
+
function HeroView({ kicker, heading, text, buttons, image, styles }: Props<typeof heroSchema>) {
|
|
91
|
+
return (
|
|
92
|
+
<smoodly.Section styles={styles} className="hero">
|
|
93
|
+
<div className="container hero__grid">
|
|
94
|
+
<div className="hero__copy">
|
|
95
|
+
{kicker && <p className="kicker" {...m.field("kicker")}>{kicker}</p>}
|
|
96
|
+
<h1 className="hero__heading" {...m.field("heading")}>{heading}</h1>
|
|
97
|
+
<p className="hero__text" {...m.field("text")}>{text}</p>
|
|
98
|
+
{buttons?.length > 0 && (
|
|
99
|
+
<div className="hero__buttons">
|
|
100
|
+
{buttons.map((b, i) => {
|
|
101
|
+
const button = m.item("buttons", i);
|
|
102
|
+
return (
|
|
103
|
+
<a key={i} className={i === 0 ? "button" : "button button--outline"} href={b.href}
|
|
104
|
+
{...button.attrs} {...button.field("label")}>{b.label}</a>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
{image && <SmoodlyImage image={image} className="hero__image" sizes="(min-width: 900px) 40vw, 100vw" priority {...m.field("image")} />}
|
|
111
|
+
</div>
|
|
112
|
+
</smoodly.Section>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Hero = smoodly.section(heroSchema, HeroView);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Three things make this a section rather than a plain component:
|
|
120
|
+
|
|
121
|
+
- **`smoodly.Section` is the root element.** Pass it `styles`, and it renders the spacing and width steps an editor picks, plus the attribute the canvas uses to select the section. `as="header"` changes the tag; `className`, `id` and `data-` attributes pass through. The section's own CSS is yours, in `app/globals.css` like the starter's.
|
|
122
|
+
- **Marks make fields click-to-edit.** `smoodly.marks(heroSchema)` returns typed helpers. Spread `m.field("heading")` on the element that renders the heading, and clicking it in the canvas opens that field. For a list, `m.item("buttons", i)` gives a scope: spread its `attrs` on the item's root and mark the fields inside with `field()`. `m.object()` does the same for an object field. Outside the admin the marks are plain `data-` attributes. [Marks and inline editing](../content-modeling/marks.md) has the details.
|
|
123
|
+
- **`smoodly.section()` pairs the two.** Rename a field in the schema and this line fails to compile until the view follows. The export is the component you list in `smoodly/views.ts`. It renders as a normal component too, so you can place a section in your own JSX with its fields as props.
|
|
124
|
+
|
|
125
|
+
### When a section throws
|
|
126
|
+
|
|
127
|
+
If a section's own code throws, for example on data it didn't expect, the page still renders and that section is left out. The error goes to the server log, and the editor's canvas shows a placeholder in its place. The editor names it too, and Publish asks before a page with a broken section goes live.
|
|
128
|
+
|
|
129
|
+
**Astro**
|
|
130
|
+
|
|
131
|
+
The zone renders each section through `Astro.slots.render` inside a try/catch, so a throw anywhere inside the section, a component it renders included, leaves the section out and the page renders.
|
|
132
|
+
|
|
133
|
+
**Next.js**
|
|
134
|
+
|
|
135
|
+
`next dev` shows the placeholder too. A throw inside a component the section renders, or in a `'use client'` section, still fails the live page, but in the canvas it is caught and named the same way, so it shows up before anyone publishes.
|
|
136
|
+
|
|
137
|
+
## Registering it
|
|
138
|
+
|
|
139
|
+
**Astro**
|
|
140
|
+
|
|
141
|
+
Add the schema to `src/smoodly/sections.ts`, which the config registers. The view is the file; there is nothing else to list. Editors can place the section only once a zone lists it, which is the next step:
|
|
142
|
+
|
|
143
|
+
File: `src/smoodly/sections.ts`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { heroSchema } from "@/components/sections/Hero.schema";
|
|
147
|
+
// …
|
|
148
|
+
export const sections = [heroSchema, pageHeroSchema, imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema, headerSchema, footerSchema];
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Next.js**
|
|
152
|
+
|
|
153
|
+
A section is registered in two lists. Add its schema to `smoodly/sections.ts`, which the config registers, and its component to `sections` in `smoodly/views.ts`. The config imports no component, so the CLI, a seed script and an agent can read your content model without loading React. Editors can place the section only once a zone lists it, which is the next step:
|
|
154
|
+
|
|
155
|
+
File: `smoodly/sections.ts`
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { heroSchema } from "@/components/sections/Hero.schema";
|
|
159
|
+
// …
|
|
160
|
+
export const sections = [heroSchema, pageHeroSchema, imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema, headerSchema, footerSchema];
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
File: `smoodly/views.ts`
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { Hero } from "@/components/sections/Hero";
|
|
167
|
+
// …
|
|
168
|
+
export const views = () =>
|
|
169
|
+
defineViews(config, {
|
|
170
|
+
sections: [Hero, PageHero, ImageText, Points, FavouriteArticles, People, Quote, Header, Footer],
|
|
171
|
+
// …
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A schema with no component in `smoodly/views.ts` stops the first render with a named error: `smoodly: section "hero" is registered in the config but has no view — add its component to defineViews({ sections }).`
|
|
176
|
+
|
|
177
|
+
Registering makes the section known; a page type's zones decide where editors may use it. Lorem Co.'s home page locks its hero and lets the body take five sections:
|
|
178
|
+
|
|
179
|
+
File: `components/pages/Home.schema.ts`
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
export const homeSchema = smoodly.schema.page({
|
|
183
|
+
name: "home",
|
|
184
|
+
title: "Home",
|
|
185
|
+
slug: "home",
|
|
186
|
+
zones: {
|
|
187
|
+
hero: z.locked(heroSchema),
|
|
188
|
+
body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
|
|
189
|
+
},
|
|
190
|
+
// …
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A registered section that no zone lists never appears in *Add section*. Lorem Co.'s Header and Footer are registered that way, because the layout renders them as [globals](../globals/header-and-footer.md). The three policies are on [Zones and freedom](../content-modeling/zones.md).
|
|
195
|
+
|
|
196
|
+
## Sample content
|
|
197
|
+
|
|
198
|
+
`sample` is what a section starts with, so a page never shows an empty one. Smoodly uses it twice:
|
|
199
|
+
|
|
200
|
+
- When an editor picks the section in *Add section*, the new section carries the sample.
|
|
201
|
+
- When a page opens with an empty locked zone, Smoodly fills the zone from the locked section's sample.
|
|
202
|
+
|
|
203
|
+
Fields the sample leaves out start from their `.default()`, or empty. A missing image or number stays unset rather than showing a broken picture or a stray zero. Make the sample a complete value in plain copy: it is what editors see first, and what they overwrite.
|
|
204
|
+
|
|
205
|
+
The sample may set settings too, under their own keys.
|
|
206
|
+
|
|
207
|
+
## Settings
|
|
208
|
+
|
|
209
|
+
Fields are content. Settings are your own options for how the section looks, which the view alone interprets. Lorem Co.'s *Image and text* lets editors put the photo on either side:
|
|
210
|
+
|
|
211
|
+
File: `components/sections/ImageText.schema.ts`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
export const imageTextSchema = smoodly.schema.section({
|
|
215
|
+
name: "imageText",
|
|
216
|
+
title: "Image and text",
|
|
217
|
+
// …
|
|
218
|
+
settings: {
|
|
219
|
+
imageSide: f.radio([{ value: "left", label: "Image left" }, { value: "right", label: "Image right" }]).default("left"),
|
|
220
|
+
},
|
|
221
|
+
// …
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Astro**
|
|
226
|
+
|
|
227
|
+
File: `src/sections/imageText.astro`
|
|
228
|
+
|
|
229
|
+
```astro
|
|
230
|
+
---
|
|
231
|
+
// …
|
|
232
|
+
const { image, heading, text, link, settings, styles } = Astro.props;
|
|
233
|
+
// …
|
|
234
|
+
---
|
|
235
|
+
<Section styles={styles} class="image-text" data-image-side={settings.imageSide}>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Next.js**
|
|
239
|
+
|
|
240
|
+
File: `components/sections/ImageText.tsx`
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
function ImageTextView({ image, heading, text, link, settings, styles }: Props<typeof imageTextSchema>) {
|
|
244
|
+
// …
|
|
245
|
+
<smoodly.Section styles={styles} className="image-text" data-image-side={settings.imageSide}>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The view receives `settings` with defaults filled, so `settings.imageSide` is always a value. Settings use the same `f` builders as fields, with one rule: they are never localized, because a layout choice is the same in every language.
|
|
249
|
+
|
|
250
|
+
In the inspector, fields are on the *Content* tab and settings on the *Settings* tab, above Smoodly's own *Spacing & layout* group. [Settings, styles and CSS](../content-modeling/styles-and-settings.md) covers that group, `styles: false` for a section that should not offer it, and the custom CSS box.
|
|
251
|
+
|
|
252
|
+
## The fields used here
|
|
253
|
+
|
|
254
|
+
| Field | Editor sees | Notes |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| `f.text()` | Single line | .max(n) limits the length in characters. |
|
|
257
|
+
| `f.link()` | Text box | A full URL or a site path such as /company/about-us. |
|
|
258
|
+
| `f.image({ aspect })` | Media picker | Upload or pick from the library, then crop to the aspect for this use. |
|
|
259
|
+
| `f.list(item)` | Repeater | Editors add, reorder and remove items. .min(n) and .max(n) count items; lists cannot nest. |
|
|
260
|
+
| `f.object({ … })` | A group of fields | One value with named parts, such as a button's label and href. |
|
|
261
|
+
| `f.richtext({ toolset })` | Text editor | Formatted text; the toolset decides which formatting and elements it offers. |
|
|
262
|
+
| `f.radio([…])` | Pills | One value from a closed set. f.select([…]) shows the same set as a dropdown. |
|
|
263
|
+
|
|
264
|
+
Next, [Page types](../content-modeling/page-types.md) shows where zones come from, and [Zones and freedom](../content-modeling/zones.md) how to set each zone's policy.
|