@phtngyn/cms 0.1.0-beta.0 → 0.1.0-beta.2

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 (181) hide show
  1. package/README.md +260 -6
  2. package/package.json +31 -16
  3. package/src/Authoring.d.ts +13 -1
  4. package/src/Configuration-Br7tE9Qt.d.ts +68 -0
  5. package/src/{AuthoringSchema-BQ5kbhVB.js → Configuration-CA_jvQKG.js} +599 -492
  6. package/src/Configuration-CA_jvQKG.js.map +1 -0
  7. package/src/Configuration.d.ts +2 -44
  8. package/src/Configuration.js +0 -1
  9. package/src/Configuration.js.map +1 -1
  10. package/src/{DocumentEngine-Da0kWotL.js → DocumentEngine-CgiBQU4g.js} +110 -84
  11. package/src/DocumentEngine-CgiBQU4g.js.map +1 -0
  12. package/src/DocumentModel-BB9igGC7.d.ts +34 -0
  13. package/src/DocumentModel.d.ts +2 -30
  14. package/src/DocumentModel.js +13 -3
  15. package/src/DocumentModel.js.map +1 -1
  16. package/src/Forms-BsJMev5_.d.ts +74 -0
  17. package/src/Forms-CfcI3tPU.js +79 -0
  18. package/src/Forms-CfcI3tPU.js.map +1 -0
  19. package/src/Forms-DxbkU3_5.js +367 -0
  20. package/src/Forms-DxbkU3_5.js.map +1 -0
  21. package/src/Forms.d.ts +35 -0
  22. package/src/Forms.js +13 -0
  23. package/src/Forms.js.map +1 -0
  24. package/src/HttpPaths-c9nLZ9H2.js +21 -0
  25. package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
  26. package/src/I18n-DZK4CkJF.d.ts +21 -0
  27. package/src/I18n.d.ts +2 -0
  28. package/src/I18n.js +1 -0
  29. package/src/Module.js +78 -81
  30. package/src/Module.js.map +1 -1
  31. package/src/S3MediaObjects-JKMAWyVB.js +2664 -0
  32. package/src/S3MediaObjects-JKMAWyVB.js.map +1 -0
  33. package/src/Seed.d.ts +6 -0
  34. package/src/Seed.js +34 -0
  35. package/src/Seed.js.map +1 -0
  36. package/src/Seo-402YumpT.d.ts +198 -0
  37. package/src/{connection-ZGtL54qY.js → connection-B-iz0Y9Y.js} +5 -7
  38. package/src/connection-B-iz0Y9Y.js.map +1 -0
  39. package/src/{installation-BNKaVjni.js → installation-DgF2M1gL.js} +1162 -2147
  40. package/src/installation-DgF2M1gL.js.map +1 -0
  41. package/src/internal/authoring-catalog.js +153 -0
  42. package/src/internal/authoring-catalog.js.map +1 -0
  43. package/src/internal/authoring-compiler.js +2 -2
  44. package/src/internal/authoring-compiler.js.map +1 -1
  45. package/src/internal/authoring-metadata.js +68 -49
  46. package/src/internal/authoring-metadata.js.map +1 -1
  47. package/src/internal/cms-config.js +0 -1
  48. package/src/internal/cms-config.js.map +1 -1
  49. package/src/internal/configuration.js +25 -13
  50. package/src/internal/configuration.js.map +1 -1
  51. package/src/internal/document-model.js +66 -20
  52. package/src/internal/document-model.js.map +1 -1
  53. package/src/internal/form-capabilities.js +12 -0
  54. package/src/internal/form-capabilities.js.map +1 -0
  55. package/src/internal/i18n.js +47 -0
  56. package/src/internal/i18n.js.map +1 -0
  57. package/src/internal/studio-app.js +66 -0
  58. package/src/internal/studio-app.js.map +1 -0
  59. package/src/location-CFKTeK1K.js +35 -0
  60. package/src/location-CFKTeK1K.js.map +1 -0
  61. package/src/{metadata-Czvi3E4K.js → metadata-BEZPWh-3.js} +3 -72
  62. package/src/metadata-BEZPWh-3.js.map +1 -0
  63. package/src/{models-DS5ClMsH.js → models-BWm1pTAf.js} +9 -31
  64. package/src/models-BWm1pTAf.js.map +1 -0
  65. package/src/models-DwYioyNV.js +726 -0
  66. package/src/models-DwYioyNV.js.map +1 -0
  67. package/src/runtime/blocks.js +30 -0
  68. package/src/runtime/blocks.js.map +1 -0
  69. package/src/runtime/collaboration.js +9 -4
  70. package/src/runtime/collaboration.js.map +1 -1
  71. package/src/runtime/delivery/DocumentRenderer.js +10 -4
  72. package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
  73. package/src/runtime/delivery/blocks.js +8 -0
  74. package/src/runtime/delivery/blocks.js.map +1 -0
  75. package/src/runtime/delivery/context.js +12 -0
  76. package/src/runtime/delivery/context.js.map +1 -0
  77. package/src/runtime/delivery/document-page.js +14 -12
  78. package/src/runtime/delivery/document-page.js.map +1 -1
  79. package/src/runtime/delivery/document.js +21 -3
  80. package/src/runtime/delivery/document.js.map +1 -1
  81. package/src/runtime/delivery/error.js +11 -0
  82. package/src/runtime/delivery/error.js.map +1 -0
  83. package/src/runtime/delivery/plugin.js +41 -0
  84. package/src/runtime/delivery/plugin.js.map +1 -0
  85. package/src/runtime/delivery/types.d.ts +5 -0
  86. package/src/runtime/delivery/useCmsDocumentSeo.d.ts +56 -0
  87. package/src/runtime/delivery/useCmsDocumentSeo.js +99 -0
  88. package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
  89. package/src/runtime/delivery/useDocument.js +5 -2
  90. package/src/runtime/delivery/useDocument.js.map +1 -1
  91. package/src/runtime/delivery/useDocumentQuery.js +5 -2
  92. package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
  93. package/src/runtime/forms/document.js +38 -0
  94. package/src/runtime/forms/document.js.map +1 -0
  95. package/src/runtime/forms/useCmsForm.js +149 -0
  96. package/src/runtime/forms/useCmsForm.js.map +1 -0
  97. package/src/runtime/i18n/translator.js +22 -0
  98. package/src/runtime/i18n/translator.js.map +1 -0
  99. package/src/runtime/i18n/useCmsI18n.d.ts +9 -0
  100. package/src/runtime/i18n/useCmsI18n.js +29 -0
  101. package/src/runtime/i18n/useCmsI18n.js.map +1 -0
  102. package/src/runtime/installation.js +1 -1
  103. package/src/runtime/plugin.js +3 -3
  104. package/src/runtime/plugin.js.map +1 -1
  105. package/src/runtime/preview/comark-document.js +0 -1
  106. package/src/runtime/preview/comark-document.js.map +1 -1
  107. package/src/runtime/preview/connection.js +1 -1
  108. package/src/runtime/preview/controller.js +37 -11
  109. package/src/runtime/preview/controller.js.map +1 -1
  110. package/src/runtime/preview/overlay.js +65 -0
  111. package/src/runtime/preview/overlay.js.map +1 -0
  112. package/src/runtime/preview/plugin.client.js +59 -61
  113. package/src/runtime/preview/plugin.client.js.map +1 -1
  114. package/src/runtime/preview/preview.css +72 -4
  115. package/src/runtime/preview/protocol.js.map +1 -1
  116. package/src/runtime/preview/store.js +1 -1
  117. package/src/runtime/preview/targets.js +0 -1
  118. package/src/runtime/preview/targets.js.map +1 -1
  119. package/src/runtime/sitemap-entries.js +17 -0
  120. package/src/runtime/sitemap-entries.js.map +1 -0
  121. package/src/runtime/sitemap.js +24 -0
  122. package/src/runtime/sitemap.js.map +1 -0
  123. package/src/runtime/studio-app.js +5 -1
  124. package/src/runtime/studio-app.js.map +1 -1
  125. package/src/runtime/studio-entry.js +36 -0
  126. package/src/runtime/studio-entry.js.map +1 -0
  127. package/src/runtime/studio-navigation.js +11 -0
  128. package/src/runtime/studio-navigation.js.map +1 -0
  129. package/src/scalars-B2lgGUnj.js +26 -0
  130. package/src/scalars-B2lgGUnj.js.map +1 -0
  131. package/src/{store-Nl4uxEXx.js → store-DjJs3I7k.js} +12 -7
  132. package/src/store-DjJs3I7k.js.map +1 -0
  133. package/studio/CreateDocumentDialog-CTeIFlPb.js +256 -0
  134. package/studio/collaboration-DcK2F6v9.js +4566 -0
  135. package/studio/documentPlanning-Bfv-d7OU.js +29820 -0
  136. package/studio/dom-to-image-more.min-D6jGGdrW.js +877 -0
  137. package/studio/editor-Bzlh3zXt.js +30827 -0
  138. package/studio/host.css +3 -0
  139. package/studio/host.js +46614 -0
  140. package/studio/rolldown-runtime-B0aSnxlc.js +20 -0
  141. package/dist/studio/assets/ActivityEntryRow-D--fRvWI.js +0 -1
  142. package/dist/studio/assets/ActivityView-D-XBBUHb.js +0 -1
  143. package/dist/studio/assets/CreateDocumentDialog-CH8UH1QM.js +0 -1
  144. package/dist/studio/assets/DocumentsView-CxRpmAxt.js +0 -34
  145. package/dist/studio/assets/DropdownMenu-BSb1l7fr.js +0 -1
  146. package/dist/studio/assets/Input-DVao38h7.js +0 -3
  147. package/dist/studio/assets/MediaView-BHrPIjxB.js +0 -1
  148. package/dist/studio/assets/Modal-VNrXGa5C.js +0 -1
  149. package/dist/studio/assets/OverviewView-DfP6-mLZ.js +0 -1
  150. package/dist/studio/assets/Select-BJZIs9sY.js +0 -1
  151. package/dist/studio/assets/SettingsView-CerHe-ua.js +0 -1
  152. package/dist/studio/assets/Skeleton-CWJNi3nS.js +0 -1
  153. package/dist/studio/assets/StudioPageHeader-DP9HDAF2.js +0 -1
  154. package/dist/studio/assets/UsersView-cP3REkH2.js +0 -1
  155. package/dist/studio/assets/collaboration-BLnCzfvV.js +0 -4
  156. package/dist/studio/assets/context-dY4fNbn3.js +0 -1
  157. package/dist/studio/assets/editor-CE07wW83.js +0 -137
  158. package/dist/studio/assets/gateway-BAyQgCse.js +0 -10
  159. package/dist/studio/assets/index-CjoaPLhZ.js +0 -28
  160. package/dist/studio/assets/index-s2rPqCmP.css +0 -2
  161. package/dist/studio/assets/render-BkxbK2St.js +0 -83
  162. package/dist/studio/assets/rolldown-runtime-DK3Fl9T5.js +0 -1
  163. package/dist/studio/assets/useMediaLibrary-D7qR7aBf.js +0 -1
  164. package/dist/studio/index.html +0 -76
  165. package/src/AuthoringSchema-BQ5kbhVB.js.map +0 -1
  166. package/src/Configuration-BGYFD2N1.js +0 -150
  167. package/src/Configuration-BGYFD2N1.js.map +0 -1
  168. package/src/DocumentEngine-Da0kWotL.js.map +0 -1
  169. package/src/HttpPaths-86qLGB90.js +0 -15
  170. package/src/HttpPaths-86qLGB90.js.map +0 -1
  171. package/src/StudioHttp-BTLLooh0.js +0 -916
  172. package/src/StudioHttp-BTLLooh0.js.map +0 -1
  173. package/src/connection-ZGtL54qY.js.map +0 -1
  174. package/src/installation-BNKaVjni.js.map +0 -1
  175. package/src/internal/component-metadata.js +0 -37
  176. package/src/internal/component-metadata.js.map +0 -1
  177. package/src/metadata-Czvi3E4K.js.map +0 -1
  178. package/src/models-Citx8jBi.js +0 -306
  179. package/src/models-Citx8jBi.js.map +0 -1
  180. package/src/models-DS5ClMsH.js.map +0 -1
  181. package/src/store-Nl4uxEXx.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 zod
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 { z } from 'zod'
33
+ import { Schema } from 'effect'
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: z.object({ title: z.string().min(1) }),
51
+ schema: Schema.Struct({ title: Schema.NonEmptyString }),
48
52
  source: '.',
49
53
  },
50
54
  },
@@ -70,21 +74,271 @@ 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
+ ## Search and sharing
78
+
79
+ 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.
80
+
81
+ 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.
82
+
83
+ 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`:
84
+
85
+ ```ts
86
+ seo: {
87
+ defaults: {
88
+ canonicalOrigin: 'https://example.com',
89
+ siteName: 'Example',
90
+ titleTemplate: '%s · Example',
91
+ defaultDescription: 'Example publishes useful things.',
92
+ index: true,
93
+ follow: true,
94
+ identity: {
95
+ type: 'Organization',
96
+ name: 'Example',
97
+ url: 'https://example.com',
98
+ },
99
+ },
100
+ capabilities: {
101
+ identityTypes: ['Organization'],
102
+ structuredDataTypes: ['WebPage', 'Article'],
103
+ generatedSocialImageTemplateIds: [],
104
+ },
105
+ },
106
+ ```
107
+
108
+ 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.
109
+
110
+ ## Collection schemas
111
+
112
+ Effect schemas work directly. The CMS derives Studio inputs from their encoded structure and runs the original decoder when validating documents, preserving defaults, transformations, and asynchronous validation. Decoders must not require external Effect services, and their output must be a JSON-compatible object.
113
+
114
+ ```ts
115
+ import { Effect, Schema } from 'effect'
116
+ import { CalendarDate, field } from '@phtngyn/cms/DocumentModel'
117
+
118
+ const pageSchema = Schema.Struct({
119
+ description: field(Schema.String.annotate({ default: '' }), { input: 'textarea' }).pipe(
120
+ Schema.withDecodingDefaultKey(Effect.succeed('')),
121
+ ),
122
+ publishedAt: Schema.optionalKey(field(CalendarDate, { label: 'Published on' })),
123
+ title: Schema.NonEmptyString,
124
+ })
125
+ ```
126
+
127
+ 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.
128
+
129
+ Other libraries are supported through a schema implementing **both Standard Schema V1 and Standard JSON Schema V1**. Standard Schema supplies validation and typed output; Standard JSON Schema's `jsonSchema.input({ target: 'draft-2020-12' })` supplies the editor structure. A library that only implements validation needs an adapter adding JSON Schema conversion; Standard Schema alone cannot generate a form.
130
+
131
+ For example, an existing Standard Schema validator can be adapted with its library's JSON Schema converters:
132
+
133
+ ```ts
134
+ const collectionSchema = {
135
+ '~standard': {
136
+ ...validator['~standard'],
137
+ jsonSchema: {
138
+ input: () => inputJsonSchema,
139
+ output: () => outputJsonSchema,
140
+ },
141
+ },
142
+ }
143
+ ```
144
+
145
+ Supply actual draft 2020-12 schemas from the chosen library, using its input and output conversion modes respectively. Foreign schemas use JSON Schema `title`, `description`, `default`, and `x-cms-input` for presentation. The CMS reads only the input structure and retains the original validator for execution.
146
+
147
+ 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.
148
+
73
149
  ## Start editing
74
150
 
75
- 1. Run `pnpm dev` and open `http://localhost:3000/_cms/app`.
151
+ 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
152
  2. Enter your initialization token and create the first administrator. Initialization then closes.
77
153
  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
154
 
79
- Studio is bundled with the module; no separate Studio deployment or auth setup is required. Readiness is available at `/_cms/health/ready`. Restart Nuxt after changing CMS configuration or component schemas.
155
+ Studio is bundled with the module; no separate Studio deployment or auth setup is required. Readiness is available at `/_cms/health/ready`. Component schemas update through HMR during development. CMS configuration changes use Nuxt’s restart lifecycle.
156
+
157
+ ## Preview drafts
158
+
159
+ Studio previews render authored documents through the same Nuxt routes, layouts, components, `useDocument`, and `useDocumentQuery` used by the website. A page can be previewed before its first publication. Preview mode follows internal navigation and survives iframe reloads; ordinary website requests continue to read published content.
160
+
161
+ Draft delivery requires the current Studio session and document access permission. Preview URLs contain only iframe coordination information, never an access token. Preview HTML and draft API responses are private and uncached. Once the page renders, the editor bridge overlays current collaborative edits without waiting for publication or replacing Nuxt HMR.
80
162
 
81
163
  ## Customize
82
164
 
83
- - Define collections with Zod in `documents.collections`. Import `field` from `@phtngyn/cms/DocumentModel` to add field labels and input hints.
165
+ - Define collections with Effect Schema in `documents.collections`. Import `field` from `@phtngyn/cms/DocumentModel` to add field labels and input hints.
84
166
  - Use the auto-imported `useDocument` and `useDocumentQuery` composables and `DocumentRenderer`, `DocumentScope`, and `DocumentField` components for custom rendering.
85
167
  - Import `defineCmsBlock` from `@phtngyn/cms/Authoring` to declare editable props and slots in your Nuxt Vue components.
86
168
  - Optionally provide `initialSources` to import Markdown documents once when the installation is first claimed.
87
169
 
170
+ For a folder of published Markdown files, import `seedFromDirectory` from `@phtngyn/cms/Seed` and set
171
+ `initialSources: seedFromDirectory(new URL('./seed/', import.meta.url))` in `cms.config.ts`.
172
+ The folder is read when Nuxt loads the configuration. Paths within it become document source paths;
173
+ for example, `seed/en/index.md` becomes `en/index.md`. Files are published on the first claim.
174
+
175
+ ## Studio and authoring during development
176
+
177
+ Studio at `/_cms/app` is a separate HTML document, Vue application, and Tailwind build. It owns its Nuxt UI settings, styles, and overlays independently of the website. Your app root and layouts apply only to the website. Both applications use the same Nuxt server; no separate Studio server or port is required.
178
+
179
+ Only components that declare `defineCmsBlock` appear in the authoring catalog. Nuxt compiles their Vue props, slots, imported types, and presentation into one generated `#cms/authoring` module. Studio’s entry imports this module through Nuxt’s Vite graph, so isolation does not require a custom HMR transport. A separate Vue component map supplies Comark’s renderer, so adding or removing a block does not reinitialize Studio. Valid semantic changes update Studio and rebuild the Nitro worker automatically, keeping the development listener running. Invalid changes show a Vite error and retain the last valid catalog.
180
+
181
+ An open editor reads the new catalog without reseeding content or replacing its collaborative document. Publishing waits until the collaboration server and editor use the same catalog revision. Installation configuration changes still use Nuxt’s configuration restart lifecycle.
182
+
183
+ ## Component authoring and collection permissions
184
+
185
+ Keep the Vue props, slots, defaults, and `defineCmsBlock` declaration together in the component's `<script setup lang="ts">`. Only declared fields and regions appear in Studio. Text regions accept plain text; rich-text regions offer only their declared `features`. Optional slots can be removed and added again. Initial content seeds new blocks; it never overwrites existing content when a declaration changes. Render optional slot wrappers conditionally to avoid empty layout space.
186
+
187
+ A collection can restrict available component blocks:
188
+
189
+ ```ts
190
+ pages: {
191
+ label: 'Page',
192
+ route: '/:path*',
193
+ schema: Schema.Struct({ title: Schema.String }),
194
+ body: {
195
+ blocks: ['content-hero'],
196
+ groups: ['Editorial'],
197
+ },
198
+ },
199
+ ```
200
+
201
+ Block names and groups form a union. Omitting `body` permits all declared blocks; `body: {}` permits none. Ordinary text remains available. Unknown selectors fail configuration validation. Studio insertion and server publication use the same document-owned rules, including blocks inside other content. A restriction change preserves existing content and reports what must be corrected before publication.
202
+
203
+ Clearing an optional property removes its stored override, so a Vue default can apply. Reset writes the current declared default. Nullable fields also offer an explicit no-value choice; empty strings, zero, and false remain actual values.
204
+
205
+ ### Images, links, and composition
206
+
207
+ String props can declare `input: 'image'` or `input: 'link'`. Studio uses the existing media library for image selection/upload and the document catalog for page links. Values remain ordinary URL strings: internal links store the selected document's public path. Image descriptions belong to a separate Vue prop; Studio does not invent an alt-text field or change your component's rendering. URL syntax and allowed schemes are checked before publication, and semantic props participate in the existing document/media reference index, including inside objects and lists. Changing a target's public path requires updating its links; references do not silently retarget by title.
208
+
209
+ 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.
210
+
211
+ Use object props for structured data and slots for composed content:
212
+
213
+ ```ts
214
+ interface Props {
215
+ settings?: { compact: boolean }
216
+ features?: Array<{ id: string; title: string; link?: string }>
217
+ }
218
+ // Inside the component's defineCmsBlock declaration:
219
+ props: {
220
+ settings: {
221
+ label: 'Settings', input: 'object',
222
+ fields: { compact: { label: 'Compact spacing', input: 'toggle' } },
223
+ },
224
+ features: {
225
+ label: 'Features', input: 'list',
226
+ fields: {
227
+ title: { label: 'Title', input: 'text' },
228
+ link: { label: 'Link', input: 'link' },
229
+ },
230
+ },
231
+ },
232
+ ```
233
+
234
+ Every list item needs a required `id: string` in its Vue type. Studio generates and preserves this value; use it as the item's Vue `:key`. Do not declare an editable control for `id`. Object/list shapes, requiredness and string choices come from Vue, including nested fields. Literal factory defaults such as `() => []` and `() => ({ compact: false })` are supported. Computed defaults and recursive types are not editable contracts.
235
+
236
+ 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.
237
+
238
+ See the example's `ContentSection.vue` for a complete component-local declaration with images, links, layout settings, repeatable features and nested blocks.
239
+
240
+ ## Headless forms
241
+
242
+ 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:
243
+
244
+ ```ts
245
+ import { defineCmsConfig } from '@phtngyn/cms/Configuration'
246
+ import { defineCmsForm } from '@phtngyn/cms/Forms'
247
+
248
+ // Inside your existing defineCmsConfig configuration:
249
+ forms: {
250
+ 'contact-form': {
251
+ definition: defineCmsForm({
252
+ label: 'Contact form',
253
+ fieldTypes: ['text', 'email', 'textarea', 'number', 'checkbox', 'select'],
254
+ rules: ['required', 'minLength', 'maxLength', 'min', 'max'],
255
+ }),
256
+ onSubmit: async ({ form, values }) => {
257
+ // Server only: deliver or persist validated answers here.
258
+ // values are keyed by stable question IDs; form.steps contains their labels.
259
+ },
260
+ },
261
+ },
262
+ ```
263
+
264
+ 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.
265
+
266
+ 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:
267
+
268
+ ```ts
269
+ import type { CmsFormDefinition } from '@phtngyn/cms/Forms'
270
+ import { useCmsForm } from '@phtngyn/cms/Forms'
271
+
272
+ const props = defineProps<{ definition: CmsFormDefinition }>()
273
+ const {
274
+ fields,
275
+ steps,
276
+ values,
277
+ errors,
278
+ currentStep,
279
+ stepIndex,
280
+ isFirstStep,
281
+ isLastStep,
282
+ isSubmitting,
283
+ isSuccess,
284
+ isPreview,
285
+ error,
286
+ next,
287
+ previous,
288
+ validate,
289
+ submit,
290
+ reset,
291
+ } = useCmsForm(() => props.definition)
292
+ ```
293
+
294
+ The composable accepts a value, ref, or getter and returns destructurable refs and functions. Bind controls to `values[field.id]`. Number answers must be numbers or null, checkbox answers booleans, and other answers strings. Empty optional answers reach the server handler as null; a required checkbox must be checked. `next()` validates the current step, `previous()` preserves answers, and `submit()` validates every step and returns whether submission succeeded. Validation errors are keyed by question ID; `_form` holds a form-wide error. Render accessible labels, error announcements, progress, and focus behavior in your application.
295
+
296
+ 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.
297
+
298
+ 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.
299
+
300
+ 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.
301
+
302
+ See `examples/nuxt/app/components/FormContents.vue` for a Tailwind-rendered example. Its server handler acknowledges submissions without storing answers.
303
+
304
+ ## Website languages
305
+
306
+ Declare languages and developer-owned website strings together in `cms.config.ts`:
307
+
308
+ ```ts
309
+ i18n: {
310
+ defaultLocale: 'en',
311
+ fallbackLocale: 'en',
312
+ prefixDefaultLocale: false,
313
+ locales: {
314
+ en: { name: 'English', messages: {
315
+ navigation: { home: 'Home' },
316
+ footer: { copyright: '© {year} Acme' },
317
+ } },
318
+ de: { name: 'Deutsch', messages: {
319
+ navigation: { home: 'Startseite' },
320
+ } },
321
+ },
322
+ },
323
+ ```
324
+
325
+ 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. Seed files import once; changing them does not synchronize an existing database.
326
+
327
+ Use the auto-imported composable in website components:
328
+
329
+ ```ts
330
+ const { t, locale } = useCmsI18n()
331
+ const navigation = computed(() => [{ label: t('navigation.home'), to: locale.value === 'de' ? '/de' : '/' }])
332
+ t('footer.copyright', { year: 2026 })
333
+ useHead({ htmlAttrs: { lang: locale } })
334
+ ```
335
+
336
+ `locale` is a read-only computed value derived from the current URL, including in Studio's website preview. Switch languages by navigating to a localized URL. For documents with different authored slugs, supply their actual destinations. Lists can explicitly pass `locale.value` to `useDocumentQuery`.
337
+
338
+ 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.
339
+
340
+ 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.
341
+
88
342
  ## License
89
343
 
90
344
  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/package.json CHANGED
@@ -14,10 +14,10 @@
14
14
  "directory": "packages/nuxt"
15
15
  },
16
16
  "name": "@phtngyn/cms",
17
- "version": "0.1.0-beta.0",
17
+ "version": "0.1.0-beta.2",
18
18
  "type": "module",
19
19
  "files": [
20
- "dist",
20
+ "studio",
21
21
  "src",
22
22
  "README.md",
23
23
  "LICENSE"
@@ -38,35 +38,50 @@
38
38
  "./DocumentModel": {
39
39
  "types": "./src/DocumentModel.d.ts",
40
40
  "import": "./src/DocumentModel.js"
41
+ },
42
+ "./Seed": {
43
+ "types": "./src/Seed.d.ts",
44
+ "import": "./src/Seed.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.1127.0",
45
- "@comark/nuxt": "^0.6.2",
46
- "@comark/vue": "^0.6.2",
47
- "@effect/platform-node-shared": "^4.0.0-rc.112",
48
- "@effect/sql-pg": "^4.0.0-rc.112",
49
- "@hocuspocus/server": "^4.6.0",
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.7",
56
- "better-auth": "^1.7.3",
57
- "comark": "^0.6.2",
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.112",
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",
64
- "nuxt-component-meta": "^0.18.0",
78
+ "nuxt-schema-org": "^6.3.1",
79
+ "nuxt-seo-utils": "^8.5.0",
65
80
  "pg": "^8.23.0",
66
- "typescript": "npm:typescript-native-bridge@6.0.3-bridge.15.tsgo.7.0.2",
81
+ "typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
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",
@@ -4,18 +4,30 @@ export type CmsBlockProp<Value> = {
4
4
  readonly label: string;
5
5
  readonly help?: string;
6
6
  } & ([NonNullable<Value>] extends [never] ? never : [NonNullable<Value>] extends [string] ? {
7
- readonly input: 'text' | 'textarea' | 'select';
7
+ readonly input: 'text' | 'textarea' | 'select' | 'image' | 'link';
8
8
  readonly optionLabels?: Partial<Record<Extract<Value, string>, string>>;
9
9
  } : [NonNullable<Value>] extends [number] ? {
10
10
  readonly input: 'number';
11
11
  } : [NonNullable<Value>] extends [boolean] ? {
12
12
  readonly input: 'toggle';
13
+ } : [NonNullable<Value>] extends [ReadonlyArray<infer Item>] ? Item extends {
14
+ readonly id: string;
15
+ } ? {
16
+ readonly input: 'list';
17
+ readonly fields: { readonly [Key in Exclude<keyof Item, 'id'>]?: CmsBlockProp<Item[Key]>; };
18
+ } : never : [NonNullable<Value>] extends [object] ? {
19
+ readonly input: 'object';
20
+ readonly fields: { readonly [Key in keyof NonNullable<Value>]?: CmsBlockProp<NonNullable<Value>[Key]>; };
13
21
  } : never);
14
22
  export type CmsBlockSlot = {
15
23
  readonly label: string;
16
24
  readonly help?: string;
17
25
  } & ({
18
26
  readonly input: 'text';
27
+ } | {
28
+ readonly input: 'blocks';
29
+ readonly blocks?: ReadonlyArray<string>;
30
+ readonly groups?: ReadonlyArray<string>;
19
31
  } | {
20
32
  readonly input: 'rich-text';
21
33
  readonly features: ReadonlyArray<'bold' | 'italic' | 'link' | 'bullet-list' | 'code'>;
@@ -0,0 +1,68 @@
1
+ import { t as CmsI18nOptions } from "./I18n-DZK4CkJF.js";
2
+ import { n as FormDefinition, s as FormValues, t as FormCapabilities } from "./Forms-BsJMev5_.js";
3
+ import { i as CmsDocumentModelOptions } from "./DocumentModel-BB9igGC7.js";
4
+ import { r as SeoConfiguration } from "./Seo-402YumpT.js";
5
+ import { Cause, Context, Crypto, DateTime, Effect, Layer, Option, Result, Schema, Scope, Stream } from "effect";
6
+ import { Extension, Node } from "@tiptap/core";
7
+ import "comark";
8
+ //#region ../../node_modules/.pnpm/prosemirror-view@1.42.3/node_modules/prosemirror-view/dist/index.d.ts
9
+ declare global {
10
+ interface Node {}
11
+ }
12
+ //#endregion
13
+ //#region ../server/src/Forms.d.ts
14
+ type FormSubmissionHandler = (submission: {
15
+ readonly form: FormDefinition;
16
+ readonly values: FormValues;
17
+ }) => void | PromiseLike<void>;
18
+ //#endregion
19
+ //#region src/Configuration.d.ts
20
+ type CmsSeoOptions = Schema.Codec.Encoded<typeof SeoConfiguration>;
21
+ interface CmsInitialDocument {
22
+ readonly markdown: string;
23
+ readonly published: boolean;
24
+ readonly sourcePath: string;
25
+ }
26
+ type CmsInitialDocumentProvider = () => PromiseLike<ReadonlyArray<CmsInitialDocument>> | ReadonlyArray<CmsInitialDocument>;
27
+ interface CmsCleanupOptions {
28
+ readonly initialDelayMillis?: number;
29
+ readonly intervalMillis?: number;
30
+ }
31
+ interface CmsMediaStorageCredentials {
32
+ readonly accessKeyId: string;
33
+ readonly secretAccessKey: string;
34
+ readonly sessionToken?: string;
35
+ }
36
+ interface CmsMediaStorageOptions {
37
+ readonly bucket: string;
38
+ readonly credentials?: CmsMediaStorageCredentials;
39
+ readonly endpoint?: string;
40
+ readonly forcePathStyle?: boolean;
41
+ readonly provisionBucket?: boolean;
42
+ readonly region: string;
43
+ }
44
+ interface CmsMediaOptions {
45
+ readonly maxUploadBytes?: number;
46
+ readonly publicBaseUrl: string;
47
+ readonly storage: CmsMediaStorageOptions;
48
+ }
49
+ interface CmsConfiguration {
50
+ readonly forms?: Readonly<Record<string, {
51
+ readonly definition: FormCapabilities;
52
+ readonly onSubmit: FormSubmissionHandler;
53
+ }>>;
54
+ readonly baseUrl: string;
55
+ readonly cleanup?: false | CmsCleanupOptions;
56
+ readonly databaseUrl: string;
57
+ readonly documents: CmsDocumentModelOptions;
58
+ readonly i18n?: CmsI18nOptions;
59
+ readonly initialSources?: CmsInitialDocumentProvider;
60
+ readonly initializationToken: string;
61
+ readonly media?: false | CmsMediaOptions;
62
+ readonly seo?: CmsSeoOptions;
63
+ readonly secret: string;
64
+ }
65
+ declare function defineCmsConfig<const Configuration extends CmsConfiguration>(configuration: Configuration): Configuration;
66
+ //#endregion
67
+ export { CmsMediaOptions as a, CmsSeoOptions as c, CmsInitialDocumentProvider as i, defineCmsConfig as l, CmsConfiguration as n, CmsMediaStorageCredentials as o, CmsInitialDocument as r, CmsMediaStorageOptions as s, CmsCleanupOptions as t };
68
+ //# sourceMappingURL=Configuration-Br7tE9Qt.d.ts.map