@phtngyn/cms 0.1.0-beta.1 → 0.1.0-beta.3
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 +212 -12
- package/bin/cms.mjs +2 -0
- package/package.json +30 -15
- package/src/{Configuration-DyKo0sLS.js → Configuration-CDMYfTre.js} +26 -147
- package/src/Configuration-CDMYfTre.js.map +1 -0
- package/src/Configuration.d.ts +24 -8
- package/src/Configuration.js +0 -1
- package/src/Configuration.js.map +1 -1
- package/src/ContentTransfer-C3Av9sWz.js +53 -0
- package/src/ContentTransfer-C3Av9sWz.js.map +1 -0
- package/src/{DocumentEngine-DYObwMDY.js → DocumentEngine-J7g-emdf.js} +75 -27
- package/src/DocumentEngine-J7g-emdf.js.map +1 -0
- package/src/DocumentModel.d.ts +8 -9
- package/src/DocumentModel.js +13 -3
- package/src/DocumentModel.js.map +1 -1
- package/src/Forms-BsJMev5_.d.ts +74 -0
- package/src/Forms-CfcI3tPU.js +79 -0
- package/src/Forms-CfcI3tPU.js.map +1 -0
- package/src/Forms-Dj6NuW66.js +367 -0
- package/src/Forms-Dj6NuW66.js.map +1 -0
- package/src/Forms.d.ts +51 -0
- package/src/Forms.js +13 -0
- package/src/Forms.js.map +1 -0
- package/src/{HttpPaths-BbS21Jy8.js → HttpPaths-c9nLZ9H2.js} +6 -2
- package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
- package/src/I18n.d.ts +20 -0
- package/src/I18n.js +1 -0
- package/src/Module.js +59 -14
- package/src/Module.js.map +1 -1
- package/src/S3MediaObjects-CpPSzJto.js +2777 -0
- package/src/S3MediaObjects-CpPSzJto.js.map +1 -0
- package/src/Seo-402YumpT.d.ts +198 -0
- package/src/cli/content.js +258 -0
- package/src/cli/content.js.map +1 -0
- package/src/cli.js +71 -0
- package/src/cli.js.map +1 -0
- package/src/{connection-CBTY-TzB.js → connection-CBAy2qGy.js} +5 -7
- package/src/connection-CBAy2qGy.js.map +1 -0
- package/src/{installation-DeuBiFRE.js → installation-CNfIriVm.js} +1007 -2246
- package/src/installation-CNfIriVm.js.map +1 -0
- package/src/internal/authoring-catalog.js +10 -4
- package/src/internal/authoring-catalog.js.map +1 -1
- package/src/internal/authoring-compiler.js +0 -1
- package/src/internal/authoring-compiler.js.map +1 -1
- package/src/internal/authoring-metadata.js +4 -4
- package/src/internal/authoring-metadata.js.map +1 -1
- package/src/internal/cms-config.js +0 -1
- package/src/internal/cms-config.js.map +1 -1
- package/src/internal/configuration.js +26 -17
- package/src/internal/configuration.js.map +1 -1
- package/src/internal/document-model.js +69 -21
- package/src/internal/document-model.js.map +1 -1
- package/src/internal/form-capabilities.js +12 -0
- package/src/internal/form-capabilities.js.map +1 -0
- package/src/internal/i18n.js +47 -0
- package/src/internal/i18n.js.map +1 -0
- package/src/internal/studio-app.js +2 -2
- package/src/internal/studio-app.js.map +1 -1
- package/src/{location-DtYv2aes.js → location-BTqdfIzX.js} +2 -3
- package/src/location-BTqdfIzX.js.map +1 -0
- package/src/{metadata-DE1Mc2td.js → metadata-BEZPWh-3.js} +3 -72
- package/src/metadata-BEZPWh-3.js.map +1 -0
- package/src/{models-C3witQBK.js → models-B4IoTmp7.js} +3 -2
- package/src/models-B4IoTmp7.js.map +1 -0
- package/src/{models-BkMgeaOL.js → models-Bah5B3bT.js} +205 -51
- package/src/models-Bah5B3bT.js.map +1 -0
- package/src/runtime/blocks.js +22 -9
- package/src/runtime/blocks.js.map +1 -1
- package/src/runtime/collaboration.js.map +1 -1
- package/src/runtime/delivery/DocumentRenderer.js +7 -4
- package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
- package/src/runtime/delivery/blocks.js +3 -1
- package/src/runtime/delivery/blocks.js.map +1 -1
- package/src/runtime/delivery/context.js +1 -1
- package/src/runtime/delivery/document-page.js +12 -12
- package/src/runtime/delivery/document-page.js.map +1 -1
- package/src/runtime/delivery/document.js +4 -2
- package/src/runtime/delivery/document.js.map +1 -1
- package/src/runtime/delivery/error.js.map +1 -1
- package/src/runtime/delivery/plugin.js +28 -2
- package/src/runtime/delivery/plugin.js.map +1 -1
- package/src/runtime/delivery/types.d.ts +5 -0
- package/src/runtime/delivery/useCmsDocumentSeo.d.ts +59 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js +101 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
- package/src/runtime/delivery/useDocument.js +2 -2
- package/src/runtime/delivery/useDocument.js.map +1 -1
- package/src/runtime/delivery/useDocumentQuery.js +2 -2
- package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
- package/src/runtime/forms/document.js +38 -0
- package/src/runtime/forms/document.js.map +1 -0
- package/src/runtime/forms/useCmsForm.js +190 -0
- package/src/runtime/forms/useCmsForm.js.map +1 -0
- package/src/runtime/i18n/translator.js +51 -0
- package/src/runtime/i18n/translator.js.map +1 -0
- package/src/runtime/i18n/useCmsI18n.d.ts +11 -0
- package/src/runtime/i18n/useCmsI18n.js +37 -0
- package/src/runtime/i18n/useCmsI18n.js.map +1 -0
- package/src/runtime/installation.js +1 -1
- package/src/runtime/plugin.js +3 -3
- package/src/runtime/plugin.js.map +1 -1
- package/src/runtime/preview/comark-document.js +0 -1
- package/src/runtime/preview/comark-document.js.map +1 -1
- package/src/runtime/preview/connection.js +1 -1
- package/src/runtime/preview/controller.js +10 -3
- package/src/runtime/preview/controller.js.map +1 -1
- package/src/runtime/preview/overlay.js.map +1 -1
- package/src/runtime/preview/plugin.client.js +59 -48
- package/src/runtime/preview/plugin.client.js.map +1 -1
- package/src/runtime/preview/protocol.js.map +1 -1
- package/src/runtime/preview/store.js +1 -1
- package/src/runtime/preview/targets.js +0 -1
- package/src/runtime/preview/targets.js.map +1 -1
- package/src/runtime/sitemap-entries.js +17 -0
- package/src/runtime/sitemap-entries.js.map +1 -0
- package/src/runtime/sitemap.js +24 -0
- package/src/runtime/sitemap.js.map +1 -0
- package/src/runtime/studio-app.js.map +1 -1
- package/src/runtime/studio-entry.js +2 -2
- package/src/runtime/studio-entry.js.map +1 -1
- package/src/runtime/studio-navigation.js +1 -1
- package/src/scalars-B2lgGUnj.js +26 -0
- package/src/scalars-B2lgGUnj.js.map +1 -0
- package/src/{store-CfFFx9Ua.js → store-b2CSXgMt.js} +12 -7
- package/src/store-b2CSXgMt.js.map +1 -0
- package/studio/{CreateDocumentDialog--L9lTfx6.js → CreateDocumentDialog-GGey1uBc.js} +44 -44
- package/studio/{collaboration-DUuu678R.js → collaboration-DcK2F6v9.js} +7 -6
- package/studio/{documentPlanning-BXX04Woe.js → documentPlanning-DRErYmBU.js} +7340 -6469
- package/studio/{editor-PexnXkJY.js → editor-Bzlh3zXt.js} +7965 -7955
- package/studio/host.css +1 -1
- package/studio/host.js +10416 -7981
- package/src/Configuration-DyKo0sLS.js.map +0 -1
- package/src/DocumentEngine-DYObwMDY.js.map +0 -1
- package/src/HttpPaths-BbS21Jy8.js.map +0 -1
- package/src/StudioHttp-jR2-bc01.js +0 -718
- package/src/StudioHttp-jR2-bc01.js.map +0 -1
- package/src/connection-CBTY-TzB.js.map +0 -1
- package/src/installation-DeuBiFRE.js.map +0 -1
- package/src/location-DtYv2aes.js.map +0 -1
- package/src/metadata-DE1Mc2td.js.map +0 -1
- package/src/models-BkMgeaOL.js.map +0 -1
- package/src/models-C3witQBK.js.map +0 -1
- package/src/store-CfFFx9Ua.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A CMS for Nuxt with an embedded Studio, collaborative editing, PostgreSQL persistence, and S3-compatible media storage. Install one module, configure your services in `cms.config.ts`, and create your first administrator in Studio. Database migrations and authentication are managed by the module.
|
|
4
4
|
|
|
5
|
+
## Working on this package
|
|
6
|
+
|
|
7
|
+
This is the consumer guide. For implementation changes, start at [Module](./src/Module.ts) for Nuxt registration, [Configuration](./src/Configuration.ts) and [configuration compilation](./src/internal/configuration.ts) for setup, [runtime](./src/runtime/) for delivery, and [package exports and scripts](./package.json) for the current public surface. Follow the feature into its owning package and tests before changing behavior; examples below illustrate usage rather than define the current contract. The [canonical example](../../examples/nuxt/README.md) exercises the module end to end.
|
|
8
|
+
|
|
5
9
|
## Install
|
|
6
10
|
|
|
7
11
|
Requires Node.js 24, Nuxt `>=4.5.0 <5`, and Vue `>=3.6.0-rc.6 <4`. Deploy as a Node server with WebSocket support; static hosting is not supported.
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
|
-
pnpm add @phtngyn/cms@beta
|
|
14
|
+
pnpm add @phtngyn/cms@beta effect
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
Register the module in `nuxt.config.ts`:
|
|
@@ -26,7 +30,7 @@ Create `cms.config.ts` beside `nuxt.config.ts`:
|
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
import { defineCmsConfig } from '@phtngyn/cms/Configuration'
|
|
29
|
-
import {
|
|
33
|
+
import { pageSchema } from './page.schema'
|
|
30
34
|
|
|
31
35
|
function env(name: string): string {
|
|
32
36
|
const value = process.env[name]
|
|
@@ -44,7 +48,7 @@ export default defineCmsConfig({
|
|
|
44
48
|
pages: {
|
|
45
49
|
label: 'Page',
|
|
46
50
|
route: '/:path*',
|
|
47
|
-
schema:
|
|
51
|
+
schema: pageSchema,
|
|
48
52
|
source: '.',
|
|
49
53
|
},
|
|
50
54
|
},
|
|
@@ -70,9 +74,100 @@ These environment variable names are examples; configuration is executable TypeS
|
|
|
70
74
|
|
|
71
75
|
For storage using the AWS credential chain, omit `credentials`. To create a missing bucket automatically, set `provisionBucket: true` and grant bucket-creation permission. Set `media: false` to start without uploads.
|
|
72
76
|
|
|
77
|
+
## Content CLI
|
|
78
|
+
|
|
79
|
+
The installed package provides a `cms` command for importing a plain Markdown folder, exporting Studio-authored Markdown, and moving content between running CMS installations. Create the first administrator in Studio, then sign in to the CLI with that administrator's email and password. The CLI prompts for the site URL, administrator email, and password when they are missing. For noninteractive use, set `CMS_URL`, `CMS_EMAIL`, and `CMS_PASSWORD`; `--url` and `--email` can replace the first two variables. Use HTTPS outside localhost.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
CMS_URL=https://source.example.com CMS_EMAIL=admin@example.com cms content export ./content
|
|
83
|
+
CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content diff ./content
|
|
84
|
+
CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content plan ./content
|
|
85
|
+
CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content apply ./content
|
|
86
|
+
CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content plan ./seed --publish
|
|
87
|
+
CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content apply ./seed --publish
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Export writes each authored source at its CMS path and a `.cms-content.json` index with content hashes and the exact-revision publication state. Commit both to Git to review changes. Export refuses to replace locally edited Markdown or index entries. It does not delete files that disappeared from the CMS; remove stale files and index entries yourself after review. `diff` compares local Markdown and publication state with the current target and reports target-only documents. `plan` validates the whole incoming set and lists creates, updates, publications, and conflicts without writing. `apply` plans again, then commits the batch atomically against the generation it just inspected. Run `plan` and `diff` before applying, especially when targeting another environment.
|
|
91
|
+
|
|
92
|
+
For a folder without `.cms-content.json`, the CLI reads nested `.md` and `.mdc` files and uses folder-relative document paths. These sources are eligible for publication with `--publish`. Imports create drafts by default. Existing content with different Markdown is a conflict until you pass `--overwrite` to `plan` and `apply`. For an exported folder, `--publish` publishes only index entries marked `published: true`; without it, they remain drafts on the target. A newly imported draft does not unpublish an older target revision. The CLI does not delete target documents or move a target-only document. A concurrent change after planning causes `apply` to fail; review and retry.
|
|
93
|
+
|
|
94
|
+
The command tree is intentionally shared: future CMS operations can be added beside `content` without separate executables.
|
|
95
|
+
|
|
96
|
+
## Search and sharing
|
|
97
|
+
|
|
98
|
+
The module installs the focused Nuxt robots, sitemap, schema.org, and SEO utility modules. Published CMS pages receive canonical, robots, social-sharing, and structured-data metadata automatically. The sitemap contains only published, indexable documents and uses their publication revision time for `lastmod`; localized variants are emitted as alternatives when they share a document key.
|
|
99
|
+
|
|
100
|
+
Administrators can edit and publish site-wide SEO defaults at `/_cms/app/seo`. Editors can override search and sharing fields per document, while omitted values inherit the site-wide revision. Draft preview uses working SEO settings; ordinary website requests use published settings.
|
|
101
|
+
|
|
102
|
+
The initial site-wide revision is derived from `baseUrl`. To seed explicit defaults and restrict the structured identity choices Studio offers, add `seo` to `cms.config.ts`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
seo: {
|
|
106
|
+
defaults: {
|
|
107
|
+
canonicalOrigin: 'https://example.com',
|
|
108
|
+
siteName: 'Example',
|
|
109
|
+
titleTemplate: '%s · Example',
|
|
110
|
+
defaultDescription: 'Example publishes useful things.',
|
|
111
|
+
index: true,
|
|
112
|
+
follow: true,
|
|
113
|
+
identity: {
|
|
114
|
+
type: 'Organization',
|
|
115
|
+
name: 'Example',
|
|
116
|
+
url: 'https://example.com',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
capabilities: {
|
|
120
|
+
identityTypes: ['Organization'],
|
|
121
|
+
structuredDataTypes: ['WebPage', 'Article'],
|
|
122
|
+
generatedSocialImageTemplateIds: [],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Keep raw robots directives and Nuxt module configuration in application code. The CMS owns semantic index/follow choices and generated page metadata; it does not accept arbitrary JSON-LD or inline HTML.
|
|
128
|
+
|
|
129
|
+
## Collection schemas
|
|
130
|
+
|
|
131
|
+
Collections use **Standard Schema V1** for validation and **Standard JSON Schema V1** for the input structure Studio renders. This lets you keep your chosen validation library. Both contracts must be available on the collection schema; Standard Schema validation alone cannot describe controls. Studio requests draft 2020-12 JSON Schema input, resolves non-recursive local references, and runs the original validator when validating documents.
|
|
132
|
+
|
|
133
|
+
For a validator that already implements both standards, pass it directly as `schema`. Otherwise, adapt its validator and input/output JSON Schema converters:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const pageSchema = {
|
|
137
|
+
'~standard': {
|
|
138
|
+
...validator['~standard'],
|
|
139
|
+
jsonSchema: {
|
|
140
|
+
input: () => inputJsonSchema,
|
|
141
|
+
output: () => outputJsonSchema,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Supply actual draft 2020-12 schemas from the chosen library, using its input and output conversion modes respectively. Studio reads the input structure; the validator remains responsible for execution. A missing input converter produces a configuration error naming Standard JSON Schema V1. Foreign schemas use JSON Schema `title`, `description`, `default`, and `x-cms-input` for presentation.
|
|
148
|
+
|
|
149
|
+
Effect schemas are also accepted directly. The CMS derives Studio inputs from their encoded structure and runs the original decoder, preserving defaults, transformations, and asynchronous validation. Decoders must not require external Effect services, and their output must be a JSON-compatible object.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { Effect, Schema } from 'effect'
|
|
153
|
+
import { CalendarDate, field } from '@phtngyn/cms/DocumentModel'
|
|
154
|
+
|
|
155
|
+
const pageSchema = Schema.Struct({
|
|
156
|
+
description: field(Schema.String.annotate({ default: '' }), { input: 'textarea' }).pipe(
|
|
157
|
+
Schema.withDecodingDefaultKey(Effect.succeed('')),
|
|
158
|
+
),
|
|
159
|
+
publishedAt: Schema.optionalKey(field(CalendarDate, { label: 'Published on' })),
|
|
160
|
+
title: Schema.NonEmptyString,
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use `Schema.optionalKey` for optional fields and `Schema.Finite` for numeric fields. An encoded `default` annotation supplies Studio's initial value; `Schema.withDecodingDefaultKey` applies that default during validation. `CalendarDate` validates calendar dates stored as `YYYY-MM-DD` strings. `field` adds presentation annotations to the encoded side of an Effect schema.
|
|
165
|
+
|
|
166
|
+
Studio supports strings, calendar dates, finite numbers, booleans, string arrays, choices, and nested objects. Non-recursive local references are resolved. Ambiguous unions, structural intersections, recursive schemas, and unsupported field shapes fail configuration rather than producing incomplete forms.
|
|
167
|
+
|
|
73
168
|
## Start editing
|
|
74
169
|
|
|
75
|
-
1.
|
|
170
|
+
1. Start your Nuxt application and open its `/_cms/app` route. In this repository, `pnpm dev` runs the canonical example at `http://localhost:3000/_cms/app`.
|
|
76
171
|
2. Enter your initialization token and create the first administrator. Initialization then closes.
|
|
77
172
|
3. Create a page in Studio, edit it, and publish it. The module provides page routing and rendering; your app must render `<NuxtPage />` in `app/app.vue` if it has a custom app shell. Existing app routes can provide custom page layouts.
|
|
78
173
|
|
|
@@ -86,14 +181,10 @@ Draft delivery requires the current Studio session and document access permissio
|
|
|
86
181
|
|
|
87
182
|
## Customize
|
|
88
183
|
|
|
89
|
-
- Define collections with
|
|
184
|
+
- Define collections with Effect Schema in `documents.collections`. Import `field` from `@phtngyn/cms/DocumentModel` to add field labels and input hints.
|
|
90
185
|
- Use the auto-imported `useDocument` and `useDocumentQuery` composables and `DocumentRenderer`, `DocumentScope`, and `DocumentField` components for custom rendering.
|
|
91
186
|
- Import `defineCmsBlock` from `@phtngyn/cms/Authoring` to declare editable props and slots in your Nuxt Vue components.
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
## License
|
|
95
|
-
|
|
96
|
-
Proprietary. Copyright (c) 2026 Phat (phtngyn). All rights reserved. Use, copying, modification, and redistribution require prior written permission. Third-party components retain their own licenses.
|
|
187
|
+
- Import Markdown folders with `cms content plan` and `cms content apply` after creating the first administrator.
|
|
97
188
|
|
|
98
189
|
## Studio and authoring during development
|
|
99
190
|
|
|
@@ -113,7 +204,7 @@ A collection can restrict available component blocks:
|
|
|
113
204
|
pages: {
|
|
114
205
|
label: 'Page',
|
|
115
206
|
route: '/:path*',
|
|
116
|
-
schema:
|
|
207
|
+
schema: Schema.Struct({ title: Schema.String }),
|
|
117
208
|
body: {
|
|
118
209
|
blocks: ['content-hero'],
|
|
119
210
|
groups: ['Editorial'],
|
|
@@ -131,6 +222,8 @@ String props can declare `input: 'image'` or `input: 'link'`. Studio uses the ex
|
|
|
131
222
|
|
|
132
223
|
Slots with `input: 'blocks'` accept nested components. Optional `blocks` and `groups` selectors restrict direct children; omitting both permits all collection-allowed blocks. Collection permissions apply at every depth. Studio offers insertion, selection, deletion and sibling reordering inside each region. Existing content is retained when a schema change makes it invalid.
|
|
133
224
|
|
|
225
|
+
For repeatable cards or chapters with formatted copy, give the parent a blocks slot restricted to an item block. Declare the item's title, icon, link, and layout choices as props, and its copy as a `rich-text` slot. Studio keeps each child block's identity and order, so the item needs no separate `id` prop. The [example section](../../examples/nuxt/app/components/documents/ContentSection.vue) hosts repeatable [cards](../../examples/nuxt/app/components/documents/ContentCard.vue); the English and German homepages show their Markdown source.
|
|
226
|
+
|
|
134
227
|
Use object props for structured data and slots for composed content:
|
|
135
228
|
|
|
136
229
|
```ts
|
|
@@ -158,4 +251,111 @@ Every list item needs a required `id: string` in its Vue type. Studio generates
|
|
|
158
251
|
|
|
159
252
|
Collaboration merges object fields and list-item fields independently. List ordering is separate from item values; simultaneous insertions at the same position retain both items in deterministic identity order. Removing an item takes precedence over concurrent edits to it. Competing edits to the same field use Yjs conflict resolution. Authoring schemas and controls update through the existing HMR path without reseeding content.
|
|
160
253
|
|
|
161
|
-
|
|
254
|
+
## Headless forms
|
|
255
|
+
|
|
256
|
+
Register a form under the kebab-case name of your Nuxt Vue renderer. `defineCmsForm` is a normal TypeScript helper, not a compiler macro. It declares the question types and validation rules editors may use:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { defineCmsConfig } from '@phtngyn/cms/Configuration'
|
|
260
|
+
import { defineCmsForm } from '@phtngyn/cms/Forms'
|
|
261
|
+
|
|
262
|
+
// Inside your existing defineCmsConfig configuration:
|
|
263
|
+
forms: {
|
|
264
|
+
'contact-form': {
|
|
265
|
+
definition: defineCmsForm({
|
|
266
|
+
label: 'Contact form',
|
|
267
|
+
fieldTypes: ['text', 'email', 'textarea', 'number', 'checkbox', 'select'],
|
|
268
|
+
rules: ['required', 'minLength', 'maxLength', 'min', 'max'],
|
|
269
|
+
}),
|
|
270
|
+
onSubmit: async ({ form, values }) => {
|
|
271
|
+
// Server only: deliver or persist validated answers here.
|
|
272
|
+
// values are keyed by stable question IDs; form.steps contains their labels.
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Create `ContactForm.vue` with an optional `cmsForm?: CmsFormDefinition` prop. The registration supplies its Studio authoring controls; do not also declare `defineCmsBlock` for that renderer. Forms appear in the Forms block group and follow collection block permissions. Editors add steps, questions, choices and permitted rules in the property panel. One step produces a single-step form. Question and option IDs are managed by Studio; labels can change without changing identities. Select answers contain option IDs. There is no website input markup or CSS supplied by CMS.
|
|
279
|
+
|
|
280
|
+
A valid authored form receives `cmsForm` from `DocumentRenderer`. Incomplete drafts do not; show a helpful placeholder until the form is valid. Mount your custom form contents only when that definition exists:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import type { CmsFormDefinition } from '@phtngyn/cms/Forms'
|
|
284
|
+
import { useCmsForm } from '@phtngyn/cms/Forms'
|
|
285
|
+
|
|
286
|
+
const props = defineProps<{ definition: CmsFormDefinition }>()
|
|
287
|
+
const {
|
|
288
|
+
fields,
|
|
289
|
+
steps,
|
|
290
|
+
values,
|
|
291
|
+
errors,
|
|
292
|
+
currentStep,
|
|
293
|
+
stepIndex,
|
|
294
|
+
isFirstStep,
|
|
295
|
+
isLastStep,
|
|
296
|
+
isSubmitting,
|
|
297
|
+
isSuccess,
|
|
298
|
+
isPreview,
|
|
299
|
+
error,
|
|
300
|
+
fieldState,
|
|
301
|
+
next,
|
|
302
|
+
previous,
|
|
303
|
+
validate,
|
|
304
|
+
submit,
|
|
305
|
+
reset,
|
|
306
|
+
} = useCmsForm(() => props.definition)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The composable accepts a value, ref, or getter and returns destructurable refs and functions. Use `fieldState(field)` for each visible question. It returns the current `value`, `error`, normalized `setValue(input)`, and `inputAttrs` with an ID, name, required state, and ARIA error/help references. Render help with `id=state.helpId` and errors with `id=state.errorId` so those references have matching elements. For example, a native control can use `v-bind="state.inputAttrs"`, `:value="state.value"`, and `@input="state.setValue($event.target.value)"`. Checkbox controls should pass their checked boolean. Valid number answers become numbers, empty answers become null, and invalid number text remains visible to validation. Empty optional answers reach the server handler as null. `values` and `errors` remain available for custom renderers; `_form` holds a form-wide error. Render accessible labels, error announcements, progress, and focus behavior in your application.
|
|
310
|
+
|
|
311
|
+
`next()` validates the current step, `previous()` preserves answers, and `submit()` validates every step and returns whether submission succeeded. A required checkbox must be checked.
|
|
312
|
+
|
|
313
|
+
Definition replacements preserve answers for matching IDs and field types within the same form, clear validation state, and cancel pending client requests. `reset()` clears answers and submission state. The composable prevents overlapping submissions and repeat submission after success until reset. Scope disposal cancels client work. Cancellation cannot undo a server handler that has already started. An optional `submit(payload, signal)` transport override supports advanced integrations; it must return `CmsFormResult` and preserve server-side validation.
|
|
314
|
+
|
|
315
|
+
The endpoint resolves the current published form and checks its exact serialized revision, including capabilities and question settings. Stale pages must reload. Draft preview validates locally without invoking the handler. Invalid structure prevents publication; invalid answers and unknown answer keys never reach the handler. Dynamic editor-created questions have runtime-validated answer types, not compile-time-known answer keys.
|
|
316
|
+
|
|
317
|
+
Requests are limited to 128 KiB and 60 submissions per minute per server process, shared across forms. Browser requests with a foreign Origin are rejected. For deployments with multiple replicas or stronger spam protection, enforce a shared limit or challenge at your edge. There is no automatic retry, durable deduplication, inbox, email delivery, upload, branching, or save-and-resume in this release. Application handlers should account for retries after an uncertain network outcome. Configuration changes use Nuxt's restart lifecycle.
|
|
318
|
+
|
|
319
|
+
See `examples/nuxt/app/components/FormContents.vue` for a Tailwind-rendered example. Its server handler acknowledges submissions without storing answers.
|
|
320
|
+
|
|
321
|
+
## Website languages
|
|
322
|
+
|
|
323
|
+
Declare languages and developer-owned website strings together in `cms.config.ts`:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
i18n: {
|
|
327
|
+
defaultLocale: 'en',
|
|
328
|
+
fallbackLocale: 'en',
|
|
329
|
+
prefixDefaultLocale: false,
|
|
330
|
+
locales: {
|
|
331
|
+
en: { name: 'English', messages: {
|
|
332
|
+
navigation: { home: 'Home' },
|
|
333
|
+
footer: { copyright: '© {year} Acme' },
|
|
334
|
+
} },
|
|
335
|
+
de: { name: 'Deutsch', messages: {
|
|
336
|
+
navigation: { home: 'Startseite' },
|
|
337
|
+
} },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Locale codes also define the first directory of document source paths: `en/index.md` serves `/`, and `de/index.md` serves `/de`. Collection source matching starts after that directory. Markdown contains the editor-authored language already; it needs no translation key. Markdown and source paths remain stored in document revisions, with collaborative working edits checkpointed into Markdown. Import a Markdown folder with the admin CLI; subsequent changes require another plan and apply, with `--overwrite` for different Markdown.
|
|
343
|
+
|
|
344
|
+
Use the auto-imported composable in website components:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const { t, locale, localePath, switchLocalePath } = useCmsI18n()
|
|
348
|
+
const navigation = computed(() => [{ label: t('navigation.home'), to: localePath('/') }])
|
|
349
|
+
const germanVersion = computed(() => switchLocalePath('de'))
|
|
350
|
+
t('footer.copyright', { year: 2026 })
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`locale` is a read-only computed value derived from the current URL, including in Studio's website preview. `localePath(path, code?)` builds a path using the configured locale prefix policy; the code defaults to the current locale. `switchLocalePath(code)` changes the current URL's prefix while preserving its query and fragment. For documents with different authored slugs, pass the destination's unprefixed path as the second argument: `switchLocalePath('de', '/ueber-uns')`. The CMS does not infer relationships between different slugs. The Nuxt module sets the document's HTML `lang` automatically. Lists can explicitly pass `locale.value` to `useDocumentQuery`.
|
|
354
|
+
|
|
355
|
+
Message keys receive generated TypeScript completion. Dictionaries contain nested plain strings; dots are reserved for key paths. Named `{parameters}` accept strings and numbers; missing parameters remain visible. Messages fall back from the URL locale to `fallbackLocale`, then to the key. Omit `fallbackLocale` (or set it to `null`) to disable fallback. The same explicit fallback policy applies to document lookup. Development warns about missing requested-locale messages. Render messages as text, and use native `Intl` for date and number formatting.
|
|
356
|
+
|
|
357
|
+
Only validated public i18n data enters the website bundle. Installation credentials and handlers stay server-side. Small dictionaries can stay inline; larger ones can be imported into the same config. Configuration changes restart Nuxt. Omitting `i18n` preserves unlocalized source paths; the composable returns an empty locale and untranslated keys.
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
|
|
361
|
+
Proprietary. Copyright (c) 2026 Phat (phtngyn). All rights reserved. Use, copying, modification, and redistribution require prior written permission. Third-party components retain their own licenses.
|
package/bin/cms.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Phat (phtngyn)",
|
|
3
|
+
"bin": {
|
|
4
|
+
"cms": "./bin/cms.mjs"
|
|
5
|
+
},
|
|
3
6
|
"description": "A Nuxt CMS with an embedded Studio, collaborative editing, PostgreSQL, and S3-compatible media storage.",
|
|
4
7
|
"homepage": "https://github.com/phtngyn/cms",
|
|
5
8
|
"keywords": [
|
|
@@ -14,9 +17,10 @@
|
|
|
14
17
|
"directory": "packages/nuxt"
|
|
15
18
|
},
|
|
16
19
|
"name": "@phtngyn/cms",
|
|
17
|
-
"version": "0.1.0-beta.
|
|
20
|
+
"version": "0.1.0-beta.3",
|
|
18
21
|
"type": "module",
|
|
19
22
|
"files": [
|
|
23
|
+
"bin",
|
|
20
24
|
"studio",
|
|
21
25
|
"src",
|
|
22
26
|
"README.md",
|
|
@@ -38,38 +42,49 @@
|
|
|
38
42
|
"./DocumentModel": {
|
|
39
43
|
"types": "./src/DocumentModel.d.ts",
|
|
40
44
|
"import": "./src/DocumentModel.js"
|
|
45
|
+
},
|
|
46
|
+
"./I18n": {
|
|
47
|
+
"types": "./src/I18n.d.ts",
|
|
48
|
+
"import": "./src/I18n.js"
|
|
49
|
+
},
|
|
50
|
+
"./Forms": {
|
|
51
|
+
"types": "./src/Forms.d.ts",
|
|
52
|
+
"import": "./src/Forms.js"
|
|
41
53
|
}
|
|
42
54
|
},
|
|
43
55
|
"dependencies": {
|
|
44
|
-
"@aws-sdk/client-s3": "^3.
|
|
45
|
-
"@comark/nuxt": "^0.
|
|
46
|
-
"@comark/vue": "^0.
|
|
47
|
-
"@effect/platform-node-shared": "^4.0.0-rc.
|
|
48
|
-
"@effect/sql-pg": "^4.0.0-rc.
|
|
49
|
-
"@hocuspocus/server": "^4.
|
|
56
|
+
"@aws-sdk/client-s3": "^3.1131.0",
|
|
57
|
+
"@comark/nuxt": "^0.7.0",
|
|
58
|
+
"@comark/vue": "^0.7.0",
|
|
59
|
+
"@effect/platform-node-shared": "^4.0.0-rc.115",
|
|
60
|
+
"@effect/sql-pg": "^4.0.0-rc.115",
|
|
61
|
+
"@hocuspocus/server": "^4.7.0",
|
|
50
62
|
"@nuxt/kit": "^4.5.2",
|
|
51
63
|
"@nuxt/schema": "^4.5.2",
|
|
64
|
+
"@nuxtjs/robots": "^6.2.2",
|
|
65
|
+
"@nuxtjs/sitemap": "^8.5.1",
|
|
52
66
|
"@tiptap/core": "^3.31.3",
|
|
53
67
|
"@tiptap/starter-kit": "^3.31.3",
|
|
54
68
|
"@tiptap/y-tiptap": "^3.0.9",
|
|
55
|
-
"@vue/compiler-sfc": "^3.6.0-rc.
|
|
56
|
-
"better-auth": "^1.7.
|
|
57
|
-
"comark": "^0.
|
|
69
|
+
"@vue/compiler-sfc": "^3.6.0-rc.8",
|
|
70
|
+
"better-auth": "^1.7.4",
|
|
71
|
+
"comark": "^0.7.0",
|
|
58
72
|
"diff": "^9.0.0",
|
|
59
73
|
"drizzle-orm": "^1.0.0-rc.5-169397b",
|
|
60
|
-
"effect": "^4.0.0-rc.
|
|
74
|
+
"effect": "^4.0.0-rc.115",
|
|
61
75
|
"h3": "^1.15.11",
|
|
62
76
|
"jiti": "^2.7.0",
|
|
63
77
|
"nitropack": "^2.13.4",
|
|
78
|
+
"nuxt-schema-org": "^6.3.1",
|
|
79
|
+
"nuxt-seo-utils": "^8.5.0",
|
|
64
80
|
"pg": "^8.23.0",
|
|
65
81
|
"typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
|
|
66
|
-
"vite": "^8.
|
|
82
|
+
"vite": "^8.3.0",
|
|
67
83
|
"vue-component-meta": "^3.3.11",
|
|
68
|
-
"yjs": "^13.6.32"
|
|
69
|
-
"zod": "^4.5.4"
|
|
84
|
+
"yjs": "^13.6.32"
|
|
70
85
|
},
|
|
71
86
|
"peerDependencies": {
|
|
72
87
|
"nuxt": ">=4.5.0 <5.0.0",
|
|
73
88
|
"vue": ">=3.6.0-rc.6 <4.0.0"
|
|
74
89
|
}
|
|
75
|
-
}
|
|
90
|
+
}
|