smoodly 0.0.60 → 0.0.61
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/files.generated.js +1 -1
- 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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# When the schema changes
|
|
2
|
+
|
|
3
|
+
Sites change after launch: a section goes, a field is renamed, a zone gets stricter. This page covers what stored content keeps when the code moves on, how the admin shows it, and what Publish refuses until an editor fixes it.
|
|
4
|
+
|
|
5
|
+
## What a change leaves behind
|
|
6
|
+
|
|
7
|
+
Smoodly never rewrites stored content when you deploy. A page, an entry or a global keeps what it held, and the admin compares it with the code you deployed. What it finds falls into two kinds: **broken** content blocks the next publish, **left over** content is only reported.
|
|
8
|
+
|
|
9
|
+
| Change | What stays stored | Publish |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `Remove a section` | Every placed copy of it. The canvas shows a placeholder; the site renders nothing for it. | Refused |
|
|
12
|
+
| `Rename or remove a field` | The old key and its value. The admin lists it under No longer in the schema; the new field starts empty. | Allowed, unless the new field is required: then Required until it is filled |
|
|
13
|
+
| `Change a field's type` | The old value. When it doesn't fit the new type, such as text in f.number(), the field shows an error. | Refused when it doesn't fit |
|
|
14
|
+
| `Remove a select option` | The value. It no longer fits the field. | Refused |
|
|
15
|
+
| `Tighten a zone` | Sections the zone no longer allows, and sections over its .max(). | Refused |
|
|
16
|
+
| `Remove a zone from a page type` | The zone's sections, no longer shown. | Allowed |
|
|
17
|
+
| `Remove an element` | Every copy of it inside richtext. Its card in the editor says the code no longer registers it. | Refused |
|
|
18
|
+
| `Drop an element from a toolset` | Every copy of it. It still renders, because a toolset only decides what editors can insert. | Allowed |
|
|
19
|
+
| `Remove a page type` | Its pages. The canvas shows the site's 404 until the page gets another type. | Refused |
|
|
20
|
+
| `Remove a collection` | Its entries. The admin has no editor for them, so register it again or delete them. | No editor |
|
|
21
|
+
| `Toggle .localized()` | Every value. See Localized fields. | Allowed |
|
|
22
|
+
|
|
23
|
+
An empty value always fits: a new required field on old content is a *Required* error, as it is for new content.
|
|
24
|
+
|
|
25
|
+
## Content health
|
|
26
|
+
|
|
27
|
+
*Settings › Content health* lists every page, entry and global that doesn't match the code. Every member can open it, because editors do most of the fixing. It checks when it opens and when you press *Rescan*.
|
|
28
|
+
|
|
29
|
+
Findings come in two sections, *Blocks publishing* and *Left over*. Each group is one change you made, like "Field `title` no longer in section `hero`", with the documents it touches under it. A row names the document, its languages and where the problem sits:
|
|
30
|
+
|
|
31
|
+
- *Draft* only: the next publish will be refused until it is fixed.
|
|
32
|
+
- *Live* only: the site runs on content the code no longer describes, and the draft is already fixed. Publishing clears it.
|
|
33
|
+
- Both: fix it in the draft, then publish.
|
|
34
|
+
|
|
35
|
+
*Open* goes to the document's editor. The page only points; fixes happen in the editors.
|
|
36
|
+
|
|
37
|
+
## In the editor
|
|
38
|
+
|
|
39
|
+
A document with findings shows a notice under the top bar: "2 problems block publishing · 1 left over from an older schema". The page editor marks each affected section in the *Page Outline*, red for broken and grey for left over, and the fixes are where the problems are:
|
|
40
|
+
|
|
41
|
+
- **An unknown section** shows as a placeholder on the canvas. Select it to see its stored values, each with *Copy*, and *Remove section*.
|
|
42
|
+
- **A value that doesn't fit** is an error on its field. Typing a new value replaces it.
|
|
43
|
+
- **Left-over keys** sit in a collapsed *No longer in the schema* group at the foot of the form, each with *Copy* and *Clear*.
|
|
44
|
+
- **A removed zone** is listed after the real zones with *Clear zone*.
|
|
45
|
+
|
|
46
|
+
Every fix is an ordinary draft edit: it autosaves, and *Discard changes* or the history undoes it.
|
|
47
|
+
|
|
48
|
+
## What Publish refuses
|
|
49
|
+
|
|
50
|
+
Publish refuses broken content with the problems listed, the same way it refuses an empty required field. Left-over content never blocks it. The check runs on the server, so an editor can't publish around it.
|
|
51
|
+
|
|
52
|
+
Live pages are not rewritten. After a deploy the stored live version stays as it was, and a section the code no longer registers renders nothing on the site. The next publish takes the fixed draft live.
|
|
53
|
+
|
|
54
|
+
## Renaming a field
|
|
55
|
+
|
|
56
|
+
Rename a field in two steps, so no editor loses a value. First put the new field in the schema where the old one was, and deploy:
|
|
57
|
+
|
|
58
|
+
File: `components/elements/Callout.schema.ts`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { f, smoodly } from "smoodly";
|
|
62
|
+
|
|
63
|
+
export const calloutSchema = smoodly.schema.section({
|
|
64
|
+
name: "callout",
|
|
65
|
+
title: "Callout",
|
|
66
|
+
fields: {
|
|
67
|
+
// was `title`: stored values stay under the old key until an editor moves them
|
|
68
|
+
heading: f.text(),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then, for each page *Content health* lists under "Field `title` no longer in section `callout`": copy the value from the *No longer in the schema* group into *Heading*, *Clear* the old key and publish.
|
|
74
|
+
|
|
75
|
+
There is no bulk rename in the admin yet. A tool that rewrote every page at once could break every live page with one wrong mapping, so its shape is still an open question. For many pages, a script can do it for your own site: walk `pages.list()`, read each page with `getTree`, move the value, write it back with `setTree` and publish, as [Content from code](../getting-started/content-from-code.md) shows. Until it runs, a required field under the new name renders empty on the live site; nothing warns a visitor.
|
|
76
|
+
|
|
77
|
+
A rename also fails the type check wherever code writes the old name (`npx tsc --noEmit`; `npx astro check` on Astro, whose `astro build` does not type-check; a dev server or a build pushes the renamed schema to the space when it starts), such as the starter's `scripts/seed.ts`: its `node()` calls are typed from the section. The seed has already run, so update the name there or delete the script.
|
|
78
|
+
|
|
79
|
+
Next, [Localized fields](../languages/localized-fields.md).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# The custom section
|
|
2
|
+
|
|
3
|
+
The one section editors lay out themselves: rows of richtext columns on a 12-column grid, offered where a zone allows it. You turn it on, pick what its columns may hold, and style the result; Smoodly owns the grid.
|
|
4
|
+
|
|
5
|
+
To write a section of your own in code, see [Sections](../content-modeling/sections.md).
|
|
6
|
+
|
|
7
|
+
## Turning it on
|
|
8
|
+
|
|
9
|
+
A zone offers the custom section when its policy says so. The starter's body zones do:
|
|
10
|
+
|
|
11
|
+
File: `components/pages/Home.schema.ts`
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
zones: {
|
|
15
|
+
hero: z.locked(heroSchema),
|
|
16
|
+
body: z.freeform({ sections: [imageTextSchema, pointsSchema, favouriteArticlesSchema, peopleSchema, quoteSchema], custom: true }),
|
|
17
|
+
},
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Only `z.freeform()` takes `custom: true`. *Custom section* then appears in *Add section* after the listed sections.
|
|
21
|
+
|
|
22
|
+
What a column may hold comes from the config's toolset. The starter gives the columns its article toolset, so a column can hold headings, lists, images and the two elements:
|
|
23
|
+
|
|
24
|
+
File: `smoodly/config.ts`
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export const config = defineConfig({
|
|
28
|
+
// …
|
|
29
|
+
custom: { toolset: article },
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Astro**
|
|
34
|
+
|
|
35
|
+
The columns render through the package's own richtext view, with images through Astro's image service and your elements by file name. There is nothing to bind.
|
|
36
|
+
|
|
37
|
+
**Next.js**
|
|
38
|
+
|
|
39
|
+
`smoodly/views.ts` binds the richtext renderer, so images render through the site's image component:
|
|
40
|
+
|
|
41
|
+
File: `smoodly/views.ts`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export const views = () =>
|
|
45
|
+
defineViews(config, {
|
|
46
|
+
// …
|
|
47
|
+
RichText,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Without `custom` in the config, the columns take the richtext default plus images. `breakpoint` sets where rows stack; the default is `48rem`.
|
|
52
|
+
|
|
53
|
+
## What editors get
|
|
54
|
+
|
|
55
|
+
A new custom section is one row with one column. Each row is a card in the inspector:
|
|
56
|
+
|
|
57
|
+
| Name | Description |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `Columns` | A stepper from one to six. Removing a column folds its content into the last one, so nothing is lost. |
|
|
60
|
+
| `Widths` | A bar dragged in twelfths, with presets such as 1/2 · 1/2 and 1/3 · 2/3. Spans always add up to 12. |
|
|
61
|
+
| `Gap` | S, M or L between the columns. |
|
|
62
|
+
| `Vertical align` | Top, Center, Bottom or Stretch. |
|
|
63
|
+
| `Reverse on mobile` | Flips the column order when the row stacks, so a photo can sit right on desktop and first on a phone. |
|
|
64
|
+
| `Column 1, Column 2, …` | A richtext editor per column, with the toolset from the config. |
|
|
65
|
+
|
|
66
|
+
Rows move up and down, and a section keeps at least one. In the canvas the section is labeled by its first row, *Custom section · 2 columns*, and clicking a column reveals its editor in the inspector. Like any section it has *Spacing & layout* and, when the config allows, *Custom CSS* on its Settings tab.
|
|
67
|
+
|
|
68
|
+
## How it renders
|
|
69
|
+
|
|
70
|
+
The section renders through the same wrapper as yours, so spacing, width, selection and the CSS box come free. Smoodly adds one scoped style per custom section, computed from the stored numbers only:
|
|
71
|
+
|
|
72
|
+
- Each row is a grid, `.smd-row`, with its spans as `fr` tracks, its gap and its vertical alignment. Rows sit 2rem apart.
|
|
73
|
+
- A row's width is `var(--smd-width, var(--container, none))`, with `var(--smd-gutter, 1rem)` at the sides, so define `--container` and `--smd-gutter` once and rows line up with your other sections.
|
|
74
|
+
- Below the breakpoint every row stacks to one column, and a reversed row stacks bottom-up.
|
|
75
|
+
|
|
76
|
+
Your stylesheet styles the content. Each column is a `.smd-col`; the starter gives it the same rules as its prose:
|
|
77
|
+
|
|
78
|
+
File: `app/globals.css`
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
/* The custom section's columns (.smd-col) read like prose. */
|
|
82
|
+
.prose, .smd-col { display: flex; flex-direction: column; gap: 20px; font-size: 17px; line-height: 1.65; color: var(--muted); }
|
|
83
|
+
.prose h2, .smd-col h2 { font-size: 32px; margin-top: 8px; }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Nothing else is required: a site with no rules for `.smd-col` still gets working columns.
|
|
87
|
+
|
|
88
|
+
## What it cannot do
|
|
89
|
+
|
|
90
|
+
A column holds richtext: text, headings, lists, images and elements. It never holds a section or another row, so the nesting stays one level deep and the canvas, drag and drop and history keep working. A layout that needs more than columns of text is a section you write.
|
|
91
|
+
|
|
92
|
+
At publish, Smoodly checks that each row has one to six columns and that its spans add up to 12. An empty column is allowed; it is spacing.
|
|
93
|
+
|
|
94
|
+
Next, [Marks and inline editing](../content-modeling/marks.md) explains how the canvas knows what to make click-to-edit.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Marks and inline editing
|
|
2
|
+
|
|
3
|
+
Marks are the attributes a section spreads on the elements that render its fields. They tell the canvas which field an editor clicked, so text edits in place and everything else opens in the inspector.
|
|
4
|
+
|
|
5
|
+
## What a mark is
|
|
6
|
+
|
|
7
|
+
`smoodly.marks(heroSchema)` returns typed helpers for one schema. `m.field("heading")` is an object with one `data-` attribute naming the field; spread it on the element that renders the heading. The section root, rendered by Smoodly's section wrapper, carries the section's own id. From a click the canvas walks up from the marked element to that root and knows which field of which section was meant.
|
|
8
|
+
|
|
9
|
+
Outside the admin the marks are plain data attributes and do nothing. The typed helpers exist so a renamed field is a type error rather than a dead click.
|
|
10
|
+
|
|
11
|
+
## Marking a field
|
|
12
|
+
|
|
13
|
+
Put the mark on the element that shows the value as stored:
|
|
14
|
+
|
|
15
|
+
**Astro**
|
|
16
|
+
|
|
17
|
+
File: `src/sections/hero.astro`
|
|
18
|
+
|
|
19
|
+
```astro
|
|
20
|
+
---
|
|
21
|
+
// …
|
|
22
|
+
const m = smoodly.marks(heroSchema);
|
|
23
|
+
---
|
|
24
|
+
// …
|
|
25
|
+
{kicker && <p class="kicker" {...m.field("kicker")}>{kicker}</p>}
|
|
26
|
+
<h1 class="hero__heading" {...m.field("heading")}>{heading}</h1>
|
|
27
|
+
<p class="hero__text" {...m.field("text")}>{text}</p>
|
|
28
|
+
// …
|
|
29
|
+
{image && <Image image={image} class="hero__image" sizes="(min-width: 900px) 40vw, 100vw" priority {...m.field("image")} />}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Next.js**
|
|
33
|
+
|
|
34
|
+
File: `components/sections/Hero.tsx`
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const m = smoodly.marks(heroSchema);
|
|
38
|
+
// …
|
|
39
|
+
{kicker && <p className="kicker" {...m.field("kicker")}>{kicker}</p>}
|
|
40
|
+
<h1 className="hero__heading" {...m.field("heading")}>{heading}</h1>
|
|
41
|
+
<p className="hero__text" {...m.field("text")}>{text}</p>
|
|
42
|
+
// …
|
|
43
|
+
{image && <SmoodlyImage image={image} className="hero__image" sizes="(min-width: 900px) 40vw, 100vw" priority {...m.field("image")} />}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Hovering a marked element outlines it in the canvas. What a click does depends on the field:
|
|
47
|
+
|
|
48
|
+
| Field | On click |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `f.text()` | Edits in place. The element becomes editable, and each keystroke updates the draft. Escape or a click elsewhere ends it. |
|
|
51
|
+
| `f.richtext()` | Edits in place, with the richtext toolbar floating above the selection. |
|
|
52
|
+
| `Everything else` | Opens the inspector on that field: the media picker for an image, the repeater for a list, the pills for a choice. |
|
|
53
|
+
|
|
54
|
+
In-place text editing needs an element with nothing but text inside. A mark on an element with child elements, such as a paragraph holding a link, falls through to the inspector. So does a mark on an element that shows the value transformed, in effect: the canvas would write the stored value over the formatted one. Leave a formatted date unmarked, or mark its container.
|
|
55
|
+
|
|
56
|
+
The image component passes data attributes to its wrapper, so a mark on it selects the image box.
|
|
57
|
+
|
|
58
|
+
## Lists and objects
|
|
59
|
+
|
|
60
|
+
A list item or an object field is a scope of its own. `m.item("buttons", i)` returns a helper for one item: spread its `attrs` on the item's root element and use its `field()` for the fields inside. The starter's Hero buttons put both on the same anchor:
|
|
61
|
+
|
|
62
|
+
**Astro**
|
|
63
|
+
|
|
64
|
+
File: `src/sections/hero.astro`
|
|
65
|
+
|
|
66
|
+
```astro
|
|
67
|
+
{buttons.map((b, i) => {
|
|
68
|
+
const button = m.item("buttons", i);
|
|
69
|
+
return <a class={i === 0 ? "button" : "button button--outline"} href={safeHref(b.href)} {...button.attrs} {...button.field("label")}>{b.label}</a>;
|
|
70
|
+
})}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Next.js**
|
|
74
|
+
|
|
75
|
+
File: `components/sections/Hero.tsx`
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
{buttons.map((b, i) => {
|
|
79
|
+
const button = m.item("buttons", i);
|
|
80
|
+
return (
|
|
81
|
+
<a key={i} className={i === 0 ? "button" : "button button--outline"} href={b.href}
|
|
82
|
+
{...button.attrs} {...button.field("label")}>{b.label}</a>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`m.object("link")` works the same way for an object field. A scope nests: `m.item("points", i).object("author")` reaches a field inside an object inside a list item.
|
|
88
|
+
|
|
89
|
+
A mark for the whole list, such as `m.field("people")` on the grid that renders a reference list, opens the list in the inspector.
|
|
90
|
+
|
|
91
|
+
## What the canvas checks
|
|
92
|
+
|
|
93
|
+
A click on a mark that names a field the schema does not have logs a warning in development, naming the section and the path, and then does nothing. Check the `data-smd-field` and `data-smd-item` attributes it names.
|
|
94
|
+
|
|
95
|
+
Globals in the canvas, such as the starter's header, footer and quote, are marked the same way. Selecting one shows its own fields in the inspector, tagged *global*, with its own *Publish*, because publishing a footer to every page is a deliberate click. [Globals](../globals/defining.md) has the details.
|
|
96
|
+
|
|
97
|
+
Next, the [Fields](../fields/overview.md) group lists every field type and what editors see for each.
|
|
@@ -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.
|