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.
Files changed (76) hide show
  1. package/README.md +3 -1
  2. package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
  3. package/dist/admin/editor/CanvasOverlay.js +3 -3
  4. package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
  5. package/dist/admin/editor/GlobalCanvas.js +159 -0
  6. package/dist/admin/editor/InsertPicker.js +1 -1
  7. package/dist/admin/files.generated.js +1 -1
  8. package/dist/admin/serialize.d.ts +2 -0
  9. package/dist/admin/serialize.js +1 -0
  10. package/dist/admin/shell/AdminApp.js +5 -5
  11. package/dist/admin/shell/GlobalForm.js +80 -7
  12. package/dist/admin/shell/GlobalList.js +1 -1
  13. package/dist/admin/shell/{IconRail.d.ts → NavBar.d.ts} +3 -3
  14. package/dist/admin/shell/NavBar.js +120 -0
  15. package/dist/admin/shell/global-canvas.d.ts +40 -0
  16. package/dist/admin/shell/global-canvas.js +60 -0
  17. package/dist/admin/shell/health-list.js +1 -1
  18. package/dist/admin/ui/primitives.js +1 -1
  19. package/dist/admin/ui/theme.js +99 -91
  20. package/dist/api-client.js +5 -3
  21. package/dist/astro/index.js +3 -3
  22. package/dist/cli/push.js +2 -3
  23. package/dist/env.js +3 -2
  24. package/dist/push.d.ts +7 -0
  25. package/dist/push.js +8 -0
  26. package/dist/space-kind.d.ts +2 -0
  27. package/dist/space-kind.js +1 -0
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/docs/collections/defining.md +57 -0
  31. package/docs/collections/entry-pages.md +168 -0
  32. package/docs/collections/ordering.md +47 -0
  33. package/docs/collections/queries.md +173 -0
  34. package/docs/content-modeling/changing-schemas.md +79 -0
  35. package/docs/content-modeling/custom-section.md +94 -0
  36. package/docs/content-modeling/marks.md +97 -0
  37. package/docs/content-modeling/overview.md +55 -0
  38. package/docs/content-modeling/page-types.md +237 -0
  39. package/docs/content-modeling/sections.md +264 -0
  40. package/docs/content-modeling/styles-and-settings.md +184 -0
  41. package/docs/content-modeling/zones.md +80 -0
  42. package/docs/fields/choices.md +42 -0
  43. package/docs/fields/conditional.md +72 -0
  44. package/docs/fields/link.md +31 -0
  45. package/docs/fields/list-and-object.md +58 -0
  46. package/docs/fields/media.md +103 -0
  47. package/docs/fields/overview.md +56 -0
  48. package/docs/fields/reference-and-tags.md +62 -0
  49. package/docs/fields/richtext.md +168 -0
  50. package/docs/fields/text.md +152 -0
  51. package/docs/frameworks/astro.md +110 -0
  52. package/docs/frameworks/nextjs.md +147 -0
  53. package/docs/getting-started/content-from-code.md +263 -0
  54. package/docs/getting-started/deploying.md +131 -0
  55. package/docs/getting-started/export-and-import.md +73 -0
  56. package/docs/getting-started/first-page.md +98 -0
  57. package/docs/getting-started/install.md +108 -0
  58. package/docs/getting-started/keys.md +40 -0
  59. package/docs/getting-started/project-structure.md +142 -0
  60. package/docs/getting-started/working-together.md +42 -0
  61. package/docs/globals/defining.md +99 -0
  62. package/docs/globals/header-and-footer.md +117 -0
  63. package/docs/index.md +83 -0
  64. package/docs/languages/language-switcher.md +103 -0
  65. package/docs/languages/locales.md +119 -0
  66. package/docs/languages/localized-fields.md +61 -0
  67. package/docs/media/images-and-crops.md +131 -0
  68. package/docs/media/library.md +40 -0
  69. package/docs/pages/drafts-and-publishing.md +44 -0
  70. package/docs/pages/page-tree.md +51 -0
  71. package/docs/pages/paths-and-slugs.md +74 -0
  72. package/docs/pages/seo.md +126 -0
  73. package/docs/pages/version-history.md +23 -0
  74. package/docs/reference/upgrading.md +312 -0
  75. package/package.json +4 -3
  76. package/dist/admin/shell/IconRail.js +0 -109
@@ -0,0 +1,108 @@
1
+ # Install with create-smoodly-app
2
+
3
+ One command creates a website with the Smoodly admin mounted, connected to a space that holds its content. At the end you have the Lorem Co. starter running on your machine.
4
+
5
+ ## Before you start
6
+
7
+ You need:
8
+
9
+ - Node.js 22.12 or newer.
10
+ - An account at [smoodly.io](https://smoodly.io/sign-in), with an email address.
11
+ - A project for the site. A new project has one space, **production**: the content the live site reads. You build the site in it; nothing is public until you deploy.
12
+
13
+ Open the project's production space. Its page shows the three values the installer asks for: the space id (`sp_…`), the secret key (`sk_…`) and the publishable key (`pk_…`). Keep the page open; the next step pastes them.
14
+
15
+ ## Create the site
16
+
17
+ Run the installer with the folder to create:
18
+
19
+ ```bash
20
+ npx create-smoodly-app my-site
21
+ ```
22
+
23
+ It asks four things:
24
+
25
+ 1. **Framework?** *Astro* renders each section from an `.astro` file and caches whole pages until an editor publishes. *Next.js* renders sections as React server components. Both get the same admin and the same content. [Frameworks](../frameworks/astro.md) has the details.
26
+ 2. **Multilingual site?** Answer no for one language. Yes asks for the locales, the first being the default (`en, fi`). You can read about this choice in [Locales and routing](../languages/locales.md).
27
+ 3. **Starter?** *Lorem Co.* is a small company site that uses every feature, which you then edit into your own. *Blank* has one of each part and nothing else: a Text section, a Button element, a `posts` collection with its entry page, a Home and a Page type. This guide uses Lorem Co.
28
+ 4. **The space's values.** Paste the space id, the secret key and the publishable key from the space page. The secret key is asked hidden. A key pasted into the wrong question is refused with a message saying which one it is.
29
+
30
+ The folder can be a name (`my-site`), a path (`../sites/my-site`) or `.` for the current empty folder. Its last part becomes the package name, so it takes lowercase letters, digits, dots, dashes and underscores.
31
+
32
+ Then it works on its own for a minute or two. It installs the packages and writes the space's values to an env file. It then pushes the site's content model to the space, fills the space with the starter's content and makes a first git commit.
33
+
34
+ > **Note:** If a step fails, the folder stays and the installer prints the rest under *Finish by hand*: the command that picks up where it stopped. A failed push, for example, finishes with `npx smoodly push && npm run seed` in the new folder.
35
+
36
+ ## Without questions
37
+
38
+ Every question has a flag, so a script or a coding agent can run the installer with no terminal to answer in:
39
+
40
+ ```bash
41
+ npx create-smoodly-app my-site --yes --framework astro --starter lorem --locales en,fi --space sp_… --secret-key sk_… --publishable-key pk_…
42
+ ```
43
+
44
+ | Flag | What it answers |
45
+ | --- | --- |
46
+ | `--yes` | Takes the default for anything not given: Astro, one language, the Lorem Co. starter. The space's values have no default, so they are still asked. |
47
+ | `--framework astro\|next` | The framework. |
48
+ | `--locales en,fi` | A multilingual site. The first locale is the default. |
49
+ | `--starter lorem\|blank` | The starter. |
50
+ | `--space` | The space id, `sp_…`. |
51
+ | `--secret-key` | The space's secret key, `sk_…`. |
52
+ | `--publishable-key` | The space's publishable key, `pk_…`. |
53
+
54
+ `npx create-smoodly-app --help` prints the same list. Without a terminal and without the three values, the installer stops and names the flags to pass.
55
+
56
+ ## Run it
57
+
58
+ When the installer prints *Ready*, start the site:
59
+
60
+ ```bash
61
+ cd my-site
62
+ ```
63
+
64
+ ```bash
65
+ npm run dev
66
+ ```
67
+
68
+ **Astro**
69
+
70
+ Open [localhost:4321](http://localhost:4321) for the site and [localhost:4321/admin](http://localhost:4321/admin) for the admin.
71
+
72
+ **Next.js**
73
+
74
+ Open [localhost:3000](http://localhost:3000) for the site and [localhost:3000/admin](http://localhost:3000/admin) for the admin.
75
+
76
+ The admin sends you to smoodly.io to sign in with your account, and back. The *Pages* list shows the Lorem Co. pages: Home, Articles (at `/posts`), and under the *Company* path About us with its Team page, and News (at `/company/news`).
77
+
78
+ ## What you got
79
+
80
+ Your content model lives in your code. The dev server and every build push it to your space, so a field you add is editable at once; nothing migrates.
81
+
82
+ **Astro**
83
+
84
+ The site keeps its settings in `.env`, Astro's own env file; the integration reads it at `astro dev` and `astro build`:
85
+
86
+ | Name | Description |
87
+ | --- | --- |
88
+ | `SMOODLY_SPACE_ID` | The space your content lives in, `sp_…`. |
89
+ | `SMOODLY_SECRET_KEY` | The server-only key the site reads content with, `sk_…`. Never expose it to the browser. |
90
+ | `SMOODLY_PUBLISHABLE_KEY` | The space's public key, `pk_…`. It reads published content and nothing else. |
91
+ | `SITE_URL` | The site's own address, `http://localhost:4321` until you deploy. The integration makes it `site`, absolute links in the page head are built from it, and the push gives it to the space. |
92
+
93
+ The rest is an ordinary Astro project. Your views live in `src/`, and the Smoodly setup lives in `src/smoodly/`.
94
+
95
+ **Next.js**
96
+
97
+ The site keeps its settings in `.env.local`:
98
+
99
+ | Name | Description |
100
+ | --- | --- |
101
+ | `SMOODLY_SPACE_ID` | The space your content lives in, `sp_…`. |
102
+ | `SMOODLY_SECRET_KEY` | The server-only key the site reads content with, `sk_…`. Never expose it to the browser. |
103
+ | `SMOODLY_PUBLISHABLE_KEY` | The space's public key, `pk_…`. It reads published content and nothing else. |
104
+ | `SITE_URL` | The site's own address, `http://localhost:3000` until you deploy. Absolute links in the page head are built from it, and the push gives it to the space. |
105
+
106
+ The rest is an ordinary Next.js app. Your components live in `components/`, and the Smoodly setup lives in `smoodly/`.
107
+
108
+ [Keys](../getting-started/keys.md) says what each value can reach. [Project structure](../getting-started/project-structure.md) walks through every file. Next, [make your first page](../getting-started/first-page.md) in the admin.
@@ -0,0 +1,40 @@
1
+ # Keys
2
+
3
+ The space id and the two keys a Smoodly site uses, where each one may live, what the public one can reach, and how to rotate or revoke a key.
4
+
5
+ ## Three values
6
+
7
+ Your space's page at smoodly.io shows the three values your env file names. The env file is `.env` in an Astro project, Astro's own env file, and `.env.local` in a Next.js app:
8
+
9
+ | Variable | Lives | Does |
10
+ | --- | --- | --- |
11
+ | `SMOODLY_SPACE_ID` | server | The space your content lives in, `sp_…`. Every read names it, and a key is refused by any other space. |
12
+ | `SMOODLY_SECRET_KEY` | server only | The space's secret key, `sk_…`. The site reads with it, drafts included, and scripts write with it: the push, the seed, `export` and `import`. It reaches this space only. Never ship it to a browser, a client component or a public repository. |
13
+ | `SMOODLY_PUBLISHABLE_KEY` | server and browser | The space's publishable key, `pk_…`. It reads published content and nothing else. |
14
+
15
+ The live site and its admin need the space id and the secret key. The admin's browser holds no key: it posts to Smoodly's API with the editor's session, and the API checks the session against the space's members. The starter's `/api/preview` route reads `SMOODLY_PREVIEW_SECRET` for draft previews outside the admin (Next.js only); leave it unset and the route refuses to turn draft mode on.
16
+
17
+ A preview space's values carry its name: the id is `sp_preview_…` (`sp_eu_preview_…` under the EU jurisdiction), and both keys carry the same `preview`. A production space's have no mark. An env file therefore shows which space it holds. A site needs `smoodly` 0.0.61 or newer to read a preview space; an older version refuses the key as not a Smoodly key. [Deploying](../getting-started/deploying.md#a-preview-space) says when to add one.
18
+
19
+ `SMOODLY_API_URL` is the address of Smoodly's API, `https://api.smoodly.io` unless you set it. Set it only to reach another service address.
20
+
21
+ ## What the publishable key can reach
22
+
23
+ Treat it as public. Someone who has it, and nothing else, can read your published content over the API with plain `fetch`, from anywhere, and the media files by their URL.
24
+
25
+ They cannot:
26
+
27
+ - read a draft: drafts take the secret key;
28
+ - write, publish, delete or reorder anything: every write takes the secret key or an editor's session;
29
+ - see who your members are, or who is editing what: presence takes a member's session, never a key;
30
+ - open the admin: it needs a member's session.
31
+
32
+ ## Rotating a key
33
+
34
+ Rotate a key from the space page when someone who should not have it may have seen it, or on a schedule of your own. The space mints a new key and the old one keeps working for the time you choose, a day unless you say otherwise, so the site serves without a gap while you put the new key in your env file and your host's settings and redeploy. You can also end the old key at once: then every site still holding it stops reading until it has the new one.
35
+
36
+ ## Revoking a leaked key
37
+
38
+ Revoke a key that has leaked. It is refused from its next request, so revoke it once the new key is live: a site still holding it answers its pages with an error naming the key. Every read sends the key again, so a corrected key works on the next request without a restart.
39
+
40
+ Next, [Deploying](../getting-started/deploying.md).
@@ -0,0 +1,142 @@
1
+ # Project structure
2
+
3
+ What create-smoodly-app puts in your project, and which files you change as you build the site. Everything is ordinary code in your repository; Smoodly adds a folder of setup and mounts the admin.
4
+
5
+ ## The files
6
+
7
+ **Astro**
8
+
9
+ A single-language Lorem Co. site:
10
+
11
+ ```
12
+ my-site/
13
+ ├── astro.config.ts the integration: smoodly() mounts the admin, the preview and the pages
14
+ ├── src/
15
+ │ ├── layouts/Layout.astro the site's document: head, header, footer
16
+ │ ├── page-types/ one .astro file per page type, by its name
17
+ │ ├── sections/ one .astro file per section
18
+ │ ├── elements/ one .astro file per element richtext may embed
19
+ │ ├── entry-pages/ one .astro file per collection with a path
20
+ │ ├── components/ every schema (Name.schema.ts) and the starter's helpers
21
+ │ ├── smoodly/ the Smoodly setup
22
+ │ ├── live.config.ts Smoodly's content as live collections
23
+ │ ├── strings.ts, messages/ the starter's fixed UI text
24
+ │ └── styles/global.css
25
+ ├── scripts/seed.ts the starter content, written from code
26
+ ├── public/images/ the starter's photos
27
+ └── .env the space's values
28
+ ```
29
+
30
+ A multilingual site adds `i18n` to `astro.config.ts`, with the same locales as `src/smoodly/config.ts`; the site checks that they agree when it starts. The root holds the usual `package.json` and `tsconfig.json`, plus a `README.md`, and an `AGENTS.md` for coding agents that `CLAUDE.md` points to.
31
+
32
+ **Next.js**
33
+
34
+ A single-language Lorem Co. site:
35
+
36
+ ```
37
+ my-site/
38
+ ├── app/
39
+ │ ├── (site)/ the website
40
+ │ │ ├── layout.tsx
41
+ │ │ └── [[...path]]/page.tsx
42
+ │ ├── (smoodly)/admin/ the admin
43
+ │ ├── api/preview/route.ts draft previews on and off
44
+ │ └── globals.css
45
+ ├── components/
46
+ │ ├── pages/ page types
47
+ │ ├── sections/ sections
48
+ │ ├── elements/ elements for richtext
49
+ │ └── … the starter's helpers: PageFrame, ArticleCard, strings,
50
+ │ and articles.ts and news.ts for their URLs
51
+ ├── messages/en.json the starter's fixed UI text
52
+ ├── smoodly/ the Smoodly setup
53
+ ├── scripts/seed.ts the starter content, written from code
54
+ ├── public/images/ the starter's photos
55
+ └── .env.local the space's values
56
+ ```
57
+
58
+ A multilingual site puts the website under `app/[locale]/` instead of `app/(site)/`, adds `i18n/` and `proxy.ts` for [next-intl](https://next-intl.dev), which handles the translated URLs, and a file in `messages/` per language. The root holds the usual `package.json`, `next.config.ts` and `tsconfig.json`, plus a `README.md`, and an `AGENTS.md` for coding agents that `CLAUDE.md` points to.
59
+
60
+ ## The website
61
+
62
+ **Astro**
63
+
64
+ There are no route files. `smoodly()` in `astro.config.ts` serves the pages at every address a page file of your own under `src/pages/` does not take. It also mounts the admin at `/admin`, and the preview address editors see the canvas through. `src/pages/404.astro` is yours: the site's "not found" page.
65
+
66
+ To serve the admin somewhere other than `/admin`, see [The admin](https://smoodly.io/docs/getting-started/admin).
67
+
68
+ **Next.js**
69
+
70
+ The site's route is one file, and it needs no editing:
71
+
72
+ | Name | Description |
73
+ | --- | --- |
74
+ | `app/(site)/[[...path]]/page.tsx` | Every CMS URL: the home, every page at any depth and every collection entry. Smoodly finds the page by its URL, renders it with its page type, and Next keeps it cached until a publish expires it. |
75
+ | `app/(site)/layout.tsx` | The site's root layout: fonts, globals.css and the footer. Yours to change. |
76
+
77
+ The admin sits in its own route group as one route file and takes no layout, so the site's layout never wraps it. To serve it somewhere other than `/admin`, see [The admin](https://smoodly.io/docs/getting-started/admin).
78
+
79
+ ## Your components
80
+
81
+ This is where you spend your time, in every framework.
82
+
83
+ **Astro**
84
+
85
+ Each section, element and page type is two files. `src/components/…/Name.schema.ts` holds the schema (the fields editors fill). A view named after the schema holds the markup: `src/sections/hero.astro` renders the section named `hero`.
86
+
87
+ | Name | Description |
88
+ | --- | --- |
89
+ | `src/page-types/` | One view per page type: which zones it renders and where. Lorem Co. has home, page, articlesIndex and newsIndex. |
90
+ | `src/sections/` | One view per section editors place in zones: hero, points, quote and the rest. The header and footer are sections too. |
91
+ | `src/elements/` | One view per element editors insert inside richtext, like callout and cta. |
92
+ | `src/entry-pages/` | One view per collection with a path: articles and news. |
93
+
94
+ A new section is its schema under `src/components/sections/`, the schema in `src/smoodly/sections.ts` and in the zone of each page type that may hold it, and its view in `src/sections/`. The site names a schema with no view, and a view with no schema, at the first request. [Sections](../content-modeling/sections.md) shows how.
95
+
96
+ **Next.js**
97
+
98
+ Each section, element and page type is two files: `Name.schema.ts` holds the schema (the fields editors fill) and `Name.tsx` the React component that renders them.
99
+
100
+ | Name | Description |
101
+ | --- | --- |
102
+ | `components/pages/` | Page types: which zones a page has and what may go in them. Lorem Co. has Home, Page, Articles, Article, News and News post. |
103
+ | `components/sections/` | Sections editors place in zones: Hero, Points, Quote and the rest. The header and footer are sections too. |
104
+ | `components/elements/` | Small components editors insert inside richtext, like Callout and Cta. |
105
+
106
+ A new section is two files here, its schema in `smoodly/sections.ts`, its component in `smoodly/views.ts`, and its schema in the zone of each page type that may hold it. [Sections](../content-modeling/sections.md) shows how.
107
+
108
+ ## The Smoodly setup
109
+
110
+ **Astro**
111
+
112
+ | Name | Description |
113
+ | --- | --- |
114
+ | `src/smoodly/config.ts` | The registry and settings: the schemas of page types, sections, collections and globals, plus locales, time zone and styles, in one defineConfig(). It imports no view. |
115
+ | `src/smoodly/sections.ts` | The list of section schemas the config registers. |
116
+ | `src/smoodly/collections.ts` | Collections such as articles and people. |
117
+ | `src/smoodly/globals.ts` | Global content such as the header and footer. |
118
+ | `src/smoodly/elements.ts` | The list of element schemas richtext may embed. |
119
+ | `src/smoodly/toolsets.ts` | What each richtext field offers. |
120
+ | `src/live.config.ts` | One loader per collection, global item, the pages and the page tree. Your views read through getLiveCollection and getLiveEntry. |
121
+
122
+ You edit all of them as the site grows. There is no file of wiring: the integration in `astro.config.ts` reads the config and finds the views by their file names.
123
+
124
+ **Next.js**
125
+
126
+ | Name | Description |
127
+ | --- | --- |
128
+ | `smoodly/config.ts` | The registry and settings: the schemas of page types, sections, collections and globals, plus locales, time zone and styles, in one defineConfig(). It imports no component. |
129
+ | `smoodly/sections.ts` | The list of section schemas the config registers. |
130
+ | `smoodly/views.ts` | The components that render the registered schemas, and the entry pages. |
131
+ | `smoodly/collections.ts` | Collections such as articles and people. |
132
+ | `smoodly/globals.ts` | Global content such as the header and footer. |
133
+ | `smoodly/elements.ts` | The list of element schemas richtext may embed. |
134
+ | `smoodly/toolsets.ts` | What each richtext field offers. |
135
+ | `smoodly/richtext.tsx` | The site's richtext renderer, bound to its elements. |
136
+ | `smoodly/server.ts` | Reads content for the site, and exports content() for writing it from code. Server-only. |
137
+
138
+ You edit the first seven as the site grows. The last two are wiring you rarely touch. The admin itself is one route file, `app/(smoodly)/admin/[[...segments]]/route.ts`.
139
+
140
+ `scripts/seed.ts` is how the starter's content got there: a script that writes pages and entries under the same rules as the admin. [Content from code](../getting-started/content-from-code.md) shows how to write your own.
141
+
142
+ Next, [make your first page](../getting-started/first-page.md).
@@ -0,0 +1,42 @@
1
+ # Working together
2
+
3
+ Who can open the admin, and what happens when two people open the same page: the admin shows who is where, and never lets one person's save silently overwrite another's.
4
+
5
+ ## Who can open the admin
6
+
7
+ Everyone who can open the admin is a member of the space, with a role:
8
+
9
+ | | Admin | Editor |
10
+ | --- | --- | --- |
11
+ | `Edit, publish, unpublish and delete content; upload and delete media` | yes | yes |
12
+ | `Open Settings` | yes | no |
13
+ | `Manage the space's members` | yes | no |
14
+
15
+ Members are managed in the project at smoodly.io; the admin's Members page lists them and links there ("Manage members at smoodly.io"). A space always keeps at least one admin, so it cannot lock itself out: a member list without one is refused.
16
+
17
+ ## Signing in
18
+
19
+ Editors sign in at smoodly.io, never on your site: `/admin` sends them there and back, and someone already signed in at smoodly.io comes straight back without a form. What your site's admin holds is a session for its own space only, renewed while they keep working and good for 30 days from their last visit. *Sign out* in the admin's account menu signs them out of this site and leaves their smoodly.io account signed in; the account menu's *My Account* opens their account at smoodly.io. Someone removed from the project is signed out of its sites at once: their next action sends them to sign in, and the others stop seeing them in presence.
20
+
21
+ ## Who is editing what
22
+
23
+ A lock after a title in Pages, in a collection or in Sections means someone else has that item open in the language you are looking at. Hover it for their name. Opening the item anyway shows a line under the top bar: *aleksi is also editing this page.* Nothing is locked; it is a heads-up.
24
+
25
+ Presence runs on Smoodly's service, over one connection per open admin tab. Without it the admin works exactly the same, minus the locks and the line; a tab that loses its connection reconnects on its own.
26
+
27
+ ## When two people save the same thing
28
+
29
+ Every save, publish, discard and restore carries the version you last loaded. If the other person saved in between, yours is refused rather than applied over theirs, and a red notice says who saved and when:
30
+
31
+ > aleksi saved a newer version at 14:32.
32
+ > Your unsaved changes here will be discarded.
33
+
34
+ The form stops taking input until you press **Load their version**, which shows what they saved. Your unsaved edits are gone; the notice says so before you press it.
35
+
36
+ Most of the time you never see that notice. When the other person saves, an open view that has no unsaved edits quietly refreshes to their version, and one with unsaved edits shows the notice at once.
37
+
38
+ Only your own edits count. Opening a new page fills its fixed sections for you, and that is saved like an edit. If the other person opened the page first and their view filled it, yours takes their version without the notice.
39
+
40
+ ## Languages are separate
41
+
42
+ Drafts are per language. Someone editing the Finnish page does not lock the English one, and saves in one language never conflict with the other.
@@ -0,0 +1,99 @@
1
+ # Defining a global
2
+
3
+ A global is content that exists once and appears on many pages: the header, the footer, the company's contact details. You declare it in code; editors edit it in one place under Globals.
4
+
5
+ ## Two kinds
6
+
7
+ `smoodly.global()` takes a name, a title, and exactly one of `section` or `fields`:
8
+
9
+ File: `smoodly/globals.ts`
10
+
11
+ ```ts
12
+ import { f, smoodly } from "smoodly";
13
+ import { headerSchema } from "@/components/sections/Header.schema";
14
+ import { footerSchema } from "@/components/sections/Footer.schema";
15
+ import { quoteSchema } from "@/components/sections/Quote.schema";
16
+
17
+ export const header = smoodly.global({ name: "header", title: "Header", section: headerSchema, layout: true });
18
+
19
+ export const footer = smoodly.global({ name: "footer", title: "Footer", section: footerSchema, layout: true });
20
+
21
+ export const contact = smoodly.global({
22
+ name: "contact",
23
+ title: "Contact details",
24
+ fields: {
25
+ email: f.text(),
26
+ phone: f.text().optional(),
27
+ address: f.text().localized(),
28
+ },
29
+ layout: true,
30
+ });
31
+
32
+ export const quote = smoodly.global({ name: "quote", title: "Quote", section: quoteSchema });
33
+
34
+ export const globals = [header, footer, contact, quote];
35
+ ```
36
+
37
+ - A **visual global** takes a registered section's schema. Its fields, settings and styles are the section's, and it renders through the section's view. The starter's header, footer and quote are visual.
38
+ - An **object global** takes fields of its own and renders nowhere by itself; your code reads it. The starter's contact details are one, read by the footer.
39
+
40
+ Register the list in the config's `globals`. A visual global's section must be in the `sections` list too, which is why the starter's header and footer sit in `smoodly/sections.ts` although no zone lists them.
41
+
42
+ ## Layout globals
43
+
44
+ `layout: true` says a global is part of every page: the header, the footer, the contact details the footer shows. On Astro the page's own read brings every layout global along, so the layout's `getLiveEntry` calls cost nothing; a page that hides the header simply does not read it. Leave the flag off a global that editors place in zones or that one view reads, such as the starter's quote or a form section used on a few pages: it is resolved into each page that places it, and a view that reads it pays one call. In development, a page render that reads an unflagged global prints a hint naming it, once. On Next the flag changes nothing: its data cache keeps every global until that global is published again.
45
+
46
+ ## In the admin
47
+
48
+ *Sections*, under ⋮ in the admin's top bar, lists every global with its kind, *Visual* or *Fields*, and its status in the selected language. A *Fields* global opens a form like an entry's, the fields in schema order. A *Visual* global opens on a canvas: a page of the site that shows it, the home page for a `layout` global and otherwise the first page that places it, with the global selected and the rest of the page dimmed. Its fields sit beside the canvas under *Content* and *Settings*, text marked for inline editing can be typed in place, and when several pages place it, *Shown on* picks the page. A visual global no page shows yet opens as a form. Either way the page autosaves, and carries the same *Publish* control, *History* and *Discard changes* as a page.
49
+
50
+ A global exists in every language from the start, so there is nothing to add or delete. Publishing is per language, as for pages. A field's value is shared across languages unless it chains `.localized()`: the starter's contact email is one value, its address is translated.
51
+
52
+ ## Reading a global
53
+
54
+ Your code reads a global by name and language, through the [reads](../collections/queries.md). It is `null` until the global is published in that language, so the caller renders nothing rather than an empty header.
55
+
56
+ **Astro**
57
+
58
+ A global item is its own live collection, `globalLoader(footer)` in `src/live.config.ts`, read with `getLiveEntry("footer", { locale })`; the value is the entry's `data`:
59
+
60
+ File: `src/layouts/Layout.astro`
61
+
62
+ ```astro
63
+ ---
64
+ // …
65
+ const [{ entry: header }, { entry: footer }, { entry: contact }] = await Promise.all([
66
+ getLiveEntry("header", { locale }),
67
+ getLiveEntry("footer", { locale }),
68
+ getLiveEntry("contact", { locale }),
69
+ ]);
70
+ // …
71
+ ---
72
+ // …
73
+ {footer && <Footer {...footer.data.fields} settings={footer.data.settings} styles={footer.data.styles} contact={contact?.data.fields} />}
74
+ ```
75
+
76
+ **Next.js**
77
+
78
+ `getGlobal` reads it:
79
+
80
+ File: `app/[locale]/layout.tsx`
81
+
82
+ ```tsx
83
+ const [footer, contact] = await Promise.all([
84
+ q().getGlobal("footer", { locale }),
85
+ q().getGlobal("contact", { locale }),
86
+ ]);
87
+ // …
88
+ {footer && <FooterView {...footer.fields} settings={footer.settings} styles={footer.styles} contact={contact?.fields} />}
89
+ ```
90
+
91
+ A global resolves to `{ locale, fields }`, and a visual global adds `settings` with defaults filled, and `styles`. Spread `fields` onto the section's view, with `settings` and `styles` beside it, and the section renders with the spacing steps an editor chose and the id the canvas uses to select it.
92
+
93
+ ## Placing a global on a page
94
+
95
+ A visual global can also be placed in a zone. `z.locked(quote)` binds a zone to it, so every page of that type shows the one quote; in a freeform or sections zone that allows the global's section, the *Add section* picker offers it under *Global*. A placed global is a link, not a copy: editing it anywhere changes it everywhere, and the canvas tags it *global*.
96
+
97
+ A page cannot be published in a language while a global it shows is unpublished there. Publishing the page does not publish the global; that is a click of its own, in the global's canvas card or on its page.
98
+
99
+ Next, [Header and footer](../globals/header-and-footer.md) walks through the starter's layout.
@@ -0,0 +1,117 @@
1
+ # Header and footer
2
+
3
+ The starter's header and footer are visual globals rendered by your layout, not sections placed on pages. This page walks through how they are defined, rendered and edited.
4
+
5
+ ## The sections
6
+
7
+ The header is a section like any other, with one difference in its schema:
8
+
9
+ File: `components/sections/Header.schema.ts`
10
+
11
+ ```ts
12
+ export const headerSchema = smoodly.schema.section({
13
+ name: "header",
14
+ title: "Header",
15
+ description: "The site header: brand and navigation.",
16
+ fields: {
17
+ brand: f.text(),
18
+ nav: f.list(f.object({ label: f.text(), href: f.link() })).localized(),
19
+ },
20
+ sample: { brand: "Lorem Co.", nav: [{ label: "Home", href: "/" }] },
21
+ // Nobody pads the header from the editor: no Spacing & layout group.
22
+ styles: false,
23
+ });
24
+ ```
25
+
26
+ `styles: false` takes away the *Spacing & layout* group, because the header's spacing is the design's. The brand is shared across languages and the navigation is localized as a whole, so each language has its own labels and links. The footer follows the same pattern with a tagline, three link columns and a copyright line.
27
+
28
+ Both are registered in `smoodly/sections.ts` and named with `smoodly.global()` in `smoodly/globals.ts`, which gives each a row under *Sections* and its `sample` as starting content. No zone lists them, so the *Add section* picker never offers them. Both are `layout: true` ([Defining a global](../globals/defining.md#layout-globals)): on Astro they arrive with the page's own read, so rendering them costs no extra call.
29
+
30
+ ## Rendering them
31
+
32
+ **Astro**
33
+
34
+ The layout reads the header, the footer and the contact details as live entries, and renders the sections' views with the values spread:
35
+
36
+ File: `src/layouts/Layout.astro`
37
+
38
+ ```astro
39
+ ---
40
+ // …
41
+ const [{ entry: header }, { entry: footer }, { entry: contact }] = await Promise.all([
42
+ getLiveEntry("header", { locale }),
43
+ getLiveEntry("footer", { locale }),
44
+ getLiveEntry("contact", { locale }),
45
+ ]);
46
+ const codes = Object.keys(locales);
47
+ const links = codes.length > 1
48
+ ? codes.map((code) => ({ code, url: href(locales[code]?.path ?? "/", code), current: code === locale }))
49
+ : undefined;
50
+ ---
51
+ // …
52
+ {header && <Header {...header.data.fields} settings={header.data.settings} styles={header.data.styles} home={href("/", locale)} locales={links} />}
53
+ <main><slot /></main>
54
+ {footer && <Footer {...footer.data.fields} settings={footer.data.settings} styles={footer.data.styles} contact={contact?.data.fields} />}
55
+ ```
56
+
57
+ The footer's view takes one prop its schema does not have, the contact object; a view is a plain `.astro` file and accepts anything. The header's language links need the page on screen, and in Astro the layout knows it: every page view passes its own `locales` map to the layout, which Astro renders after the page.
58
+
59
+ **Next.js**
60
+
61
+ The root layout reads the footer and the contact details and renders the footer's view:
62
+
63
+ File: `app/[locale]/layout.tsx`
64
+
65
+ ```tsx
66
+ import { FooterView } from "@/components/sections/Footer";
67
+ import { q } from "@/smoodly/server";
68
+ // …
69
+ const [footer, contact] = await Promise.all([
70
+ q().getGlobal("footer", { locale }),
71
+ q().getGlobal("contact", { locale }),
72
+ ]);
73
+ return (
74
+ <html lang={locale} className={`${serif.variable} ${sans.variable}`}>
75
+ <body>
76
+ <NextIntlClientProvider>
77
+ {children}
78
+ {footer && <FooterView {...footer.fields} settings={footer.settings} styles={footer.styles} contact={contact?.fields} />}
79
+ </NextIntlClientProvider>
80
+ </body>
81
+ </html>
82
+ );
83
+ ```
84
+
85
+ The layout renders `FooterView`, the view, rather than `Footer`, the paired component, because it passes one prop the footer's schema does not have: the contact object. A paired component accepts only its schema's fields; a view is a plain component and accepts anything. The footer exports both.
86
+
87
+ The header is not in the layout. A layout cannot know which page is on screen, and the language links in the header must land on the same page in the other language. So every page type renders the header itself, through the starter's `PageFrame`, from the page's own language map:
88
+
89
+ File: `components/PageFrame.tsx`
90
+
91
+ ```tsx
92
+ export async function PageFrame({ locale, locales, children }: {
93
+ locale: string;
94
+ locales: Record<string, LocaleAlternate | null>;
95
+ children: ReactNode;
96
+ }) {
97
+ const header = await q().getGlobal("header", { locale });
98
+ const codes = Object.keys(locales);
99
+ const links: LocaleLink[] | undefined = codes.length > 1
100
+ ? codes.map((code) => ({ code, url: href(locales[code]?.path ?? "/", code), current: code === locale }))
101
+ : undefined;
102
+ return (
103
+ <>
104
+ {header && <HeaderView {...header.fields} settings={header.settings} styles={header.styles} home={href("/", locale)} locales={links} />}
105
+ <main>{children}</main>
106
+ </>
107
+ );
108
+ }
109
+ ```
110
+
111
+ On a single-language site there are no links and the header shows the brand and navigation alone. [Language switcher and hreflang](../languages/language-switcher.md) covers the `locales` map.
112
+
113
+ ## Editing them
114
+
115
+ Editors change the header and footer under *Sections*, or in any page's canvas: both are selectable there, tagged *global*, with their own form and their own *Publish* in the inspector. The header's navigation edits in place like any list with [marks](../content-modeling/marks.md). Publishing a page never publishes the footer, because a footer going live on every page is a deliberate click.
116
+
117
+ Next, [The media library](../media/library.md).
package/docs/index.md ADDED
@@ -0,0 +1,83 @@
1
+ # Smoodly
2
+
3
+ > Smoodly is a code-first CMS for Astro and Next.js. A developer registers sections, collections and page types in code, editors build pages in a visual admin mounted in the site at /admin, and the content lives in Smoodly Cloud, read by the site over the API.
4
+
5
+ Start a site with the installer. It creates a fresh app; it does not add Smoodly to an existing one. With these flags it asks nothing:
6
+
7
+ npx create-smoodly-app <folder> --yes --framework astro|next --starter blank|lorem [--locales en,fi] --space <sp_…> --secret-key <sk_…> --publishable-key <pk_…>
8
+
9
+ The three space values are on the project's production space page at smoodly.io. In an installed site, the guides for that version are in node_modules/smoodly/docs/ and the project's AGENTS.md holds the working rules.
10
+
11
+ ## Getting started
12
+
13
+ - [Install with create-smoodly-app](getting-started/install.md): One command creates a website with the Smoodly admin mounted, connected to a space that holds its content. At the end you have the Lorem Co. starter running on your machine.
14
+ - [Project structure](getting-started/project-structure.md): What create-smoodly-app puts in your project, and which files you change as you build the site. Everything is ordinary code in your repository; Smoodly adds a folder of setup and mounts the admin.
15
+ - [Your first page](getting-started/first-page.md): Create a page in the admin, fill it with sections and publish it, as an editor would. Then look at the code that decided what the editor was allowed to do.
16
+ - [Content from code](getting-started/content-from-code.md): Write pages, entries, globals and images from a script instead of the admin: to seed a new site, or to move a client's old content in. At the end you have a script you can run again after it stops halfway.
17
+ - [Working together](getting-started/working-together.md): Who can open the admin, and what happens when two people open the same page: the admin shows who is where, and never lets one person's save silently overwrite another's.
18
+ - [Keys](getting-started/keys.md): The space id and the two keys a Smoodly site uses, where each one may live, what the public one can reach, and how to rotate or revoke a key.
19
+ - [Deploying](getting-started/deploying.md): Put your Smoodly site on the internet: give it the production space's values and let its build push the schema; then, once it is live, add a preview space to develop against.
20
+ - [Export and import](getting-started/export-and-import.md): Copy a space's content into a folder, and from the folder into a new space: every page, entry, version and media file. It is how you move a site or keep a copy.
21
+
22
+ ## Content modeling
23
+
24
+ - [Modeling content](content-modeling/overview.md): 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.
25
+ - [Defining a section](content-modeling/sections.md): 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.
26
+ - [Defining a page type](content-modeling/page-types.md): 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.
27
+ - [Zones and freedom](content-modeling/zones.md): A zone is a place in a page type where editors add sections. Its policy sets how much they may change there, from filling one section's fields to placing any section in any order.
28
+ - [Settings, styles and CSS](content-modeling/styles-and-settings.md): Beyond its fields, a placed section carries three more things editors can change: the settings you declare, Smoodly's own spacing and width steps, and a custom CSS box you can turn on. All three sit on the Settings tab.
29
+ - [The custom section](content-modeling/custom-section.md): 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.
30
+ - [Marks and inline editing](content-modeling/marks.md): 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.
31
+ - [When the schema changes](content-modeling/changing-schemas.md): 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.
32
+
33
+ ## Fields
34
+
35
+ - [All fields](fields/overview.md): Every field builder on f, what editors see for each, and the chain of modifiers they share. The pages that follow take them a few at a time.
36
+ - [Text, number, date and time](fields/text.md): The plain values: a line of text, a number, a date, a date with a time, and the slug that becomes part of an entry's URL.
37
+ - [Richtext and elements](fields/richtext.md): Formatted text stored as structured data, the toolset that decides what editors may put in it, and the elements they can insert between the paragraphs.
38
+ - [Link](fields/link.md): A link is a text box that takes a full URL or a path on your site. Pair it with a label in an object to make a button.
39
+ - [Choices](fields/choices.md): A switch, a dropdown, a row of pills or a checkbox list: the fields that pick from a closed set you declare.
40
+ - [Image, video and file](fields/media.md): The three media fields, the resolved values your view receives, and the components that render them.
41
+ - [List and object](fields/list-and-object.md): Group fields into one value with an object, or let editors repeat a value with a list. The two nest one level, which is as far as a sidebar form stays readable.
42
+ - [Reference and tags](fields/reference-and-tags.md): A reference points at an entry in a collection and arrives in the view already loaded. Tags are short labels editors reuse across entries.
43
+ - [Conditional fields](fields/conditional.md): Hide a field while another field has a certain value, so editors see only the fields that apply.
44
+
45
+ ## Pages
46
+
47
+ - [The page tree](pages/page-tree.md): How editors create, arrange and remove pages in the admin, and what your configuration allows them to do.
48
+ - [Paths and slugs](pages/paths-and-slugs.md): A page's URL is derived from its place in the page tree: each item contributes one slug. This page covers what a slug may be, which URLs your code reserves, how a URL changes and how old addresses redirect.
49
+ - [Drafts and publishing](pages/drafts-and-publishing.md): Every change saves as a draft on its own. Publish makes the draft live, after Smoodly checks it. This page covers the three states, what publish checks, and the two ways back.
50
+ - [Version history](pages/version-history.md): Every publish keeps a version. Editors look back at any of them and restore one as the new draft, without touching what is live.
51
+ - [SEO and social](pages/seo.md): A page type can give its pages an SEO & Social tab: a title, a description, a social image and a switch to hide the page from search engines. Smoodly turns them into the page's metadata.
52
+
53
+ ## Collections
54
+
55
+ - [Defining a collection](collections/defining.md): A collection is a content type with a fixed shape and many entries: posts, people, products. You declare its fields once; editors get a list, a form and publishing for it.
56
+ - [Entry pages](collections/entry-pages.md): Give a collection a path and each entry gets its own URL. The entry page is the component that renders one entry there.
57
+ - [Queries](collections/queries.md): Read entries, globals and the page tree from your own views, typed by your registry, cached, and aware of drafts.
58
+ - [Ordering](collections/ordering.md): A collection has one order, set in its schema, that the admin's list and your queries share.
59
+
60
+ ## Globals
61
+
62
+ - [Defining a global](globals/defining.md): A global is content that exists once and appears on many pages: the header, the footer, the company's contact details. You declare it in code; editors edit it in one place under Globals.
63
+ - [Header and footer](globals/header-and-footer.md): The starter's header and footer are visual globals rendered by your layout, not sections placed on pages. This page walks through how they are defined, rendered and edited.
64
+
65
+ ## Media
66
+
67
+ - [The media library](media/library.md): Every image, video and file editors upload lives in one library. A field stores a reference to an asset, so one photo can appear in many places and be found from each of them.
68
+ - [Images and crops](media/images-and-crops.md): An image field stores which asset to show and how to frame it. Smoodly's media host cuts the framed area at the width the layout needs, in a modern format, and the image component renders it without layout shift.
69
+
70
+ ## Languages
71
+
72
+ - [Locales and routing](languages/locales.md): A site publishes in one language or several. This page covers declaring the languages, how each gets its own URLs, and what editors see when a page exists in some languages and not others.
73
+ - [Localized fields](languages/localized-fields.md): On an entry or a global, a field is shared across languages unless it chains .localized(). This page covers the rule, where it applies and what it means for queries.
74
+ - [Language switcher and hreflang](languages/language-switcher.md): Every page and entry knows its own address in every language. A language switcher links from that map, and the site's hreflang tags come from the same one.
75
+
76
+ ## Frameworks
77
+
78
+ - [Astro](frameworks/astro.md): How Smoodly plugs into an Astro project: one integration in astro.config, views by file name, reads as live collections, pages cached whole and expired by tag, and what happens on a request. The installer writes all of it; this page is for reading it.
79
+ - [Next.js](frameworks/nextjs.md): How Smoodly plugs into a Next.js app: the one route file, the server seam, the admin route group, and what happens on a request. The installer writes all of it; this page is for reading it.
80
+
81
+ ## Reference
82
+
83
+ - [Upgrading](reference/upgrading.md): Update the package and deploy. Your content model lives in your code and every build pushes it to your space: there is no migration to run.