@plutocms/docs 0.0.0-stage → 0.15.0

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 (61) hide show
  1. package/.gitattributes +1 -0
  2. package/.prettierignore +5 -0
  3. package/CHANGELOG.md +178 -0
  4. package/FEATURES.md +30 -0
  5. package/README.md +39 -2
  6. package/app/assets/css/pluto-layer.css +40 -0
  7. package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
  8. package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
  9. package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -0
  10. package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
  11. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
  12. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
  13. package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +85 -0
  14. package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
  15. package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -0
  16. package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
  17. package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
  18. package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
  19. package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
  20. package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
  21. package/app/components/PlutoContentPreview.vue +35 -0
  22. package/app/composables/pluto-block-editor.ts +443 -0
  23. package/app/composables/pluto-content-edit-link.ts +19 -0
  24. package/app/composables/pluto-content-preview.ts +29 -0
  25. package/app/composables/pluto-content-runtime.ts +237 -0
  26. package/app/pages/_pluto/preview.vue +555 -0
  27. package/app/plugins/pluto-docs.ts +50 -0
  28. package/i18n/locales/en.json +253 -0
  29. package/i18n/locales/pt-BR.json +289 -0
  30. package/modules/pluto-docs/index.ts +123 -0
  31. package/modules/pluto-docs/manifest.ts +159 -0
  32. package/modules/pluto-docs/options.ts +224 -0
  33. package/nuxt.config.ts +36 -0
  34. package/package.json +55 -4
  35. package/server/handlers/navigation.get.ts +7 -0
  36. package/server/handlers/page.get.ts +7 -0
  37. package/server/handlers/surround.get.ts +7 -0
  38. package/server/plugins/pluto-docs.ts +25 -0
  39. package/server/utils/pluto-docs-runtime.ts +504 -0
  40. package/shared/types/docs.d.ts +40 -0
  41. package/shared/types/pluto-manifest.d.ts +6 -0
  42. package/shared/utils/blocks/editor.ts +357 -0
  43. package/shared/utils/blocks/history.ts +94 -0
  44. package/shared/utils/blocks/mdc-props.ts +321 -0
  45. package/shared/utils/blocks/operations.ts +692 -0
  46. package/shared/utils/blocks/parse.ts +444 -0
  47. package/shared/utils/blocks/presets.ts +383 -0
  48. package/shared/utils/blocks/preview-marks.ts +98 -0
  49. package/shared/utils/blocks/preview-protocol.ts +195 -0
  50. package/shared/utils/blocks/registry.ts +163 -0
  51. package/shared/utils/blocks/serialize.ts +107 -0
  52. package/shared/utils/blocks/tree.ts +85 -0
  53. package/shared/utils/blocks/types.ts +165 -0
  54. package/shared/utils/content-route.ts +40 -0
  55. package/shared/utils/docs-type.ts +290 -0
  56. package/shared/utils/mdc-snippets.ts +114 -0
  57. package/shared/utils/navigation-config.ts +19 -0
  58. package/shared/utils/runtime-content.ts +403 -0
  59. package/tailwind.lint.css +10 -0
  60. package/tsconfig.json +18 -0
  61. package/vitest.config.ts +7 -0
@@ -0,0 +1,159 @@
1
+ // The build manifest of the hybrid mode. Node code that runs at build time. It lists the
2
+ // content files of each type with the version that the content store reports for the file
3
+ // (`_version`). At run time the server compares the store with this list to see which pages
4
+ // changed after the build. See the docs-setup skill.
5
+
6
+ import { Buffer } from 'node:buffer'
7
+ import { createHash } from 'node:crypto'
8
+ import { readdirSync, readFileSync } from 'node:fs'
9
+ import { join, relative, sep } from 'node:path'
10
+
11
+ /** Content id → version, for one type. */
12
+ export type TypeManifest = Record<string, string>
13
+
14
+ /** Type name → its manifest. */
15
+ export type ContentManifest = Record<string, TypeManifest>
16
+
17
+ /** The same dirs that the CMS fs store skips. */
18
+ const SKIPPED_DIRS = new Set([
19
+ 'node_modules',
20
+ '.nuxt',
21
+ '.output',
22
+ '.data',
23
+ '.nitro',
24
+ '.cache',
25
+ 'dist',
26
+ ])
27
+ const SEGMENT_PATTERN = /^(?:\d+\.)?[a-z0-9]+(?:-[a-z0-9]+)*$/
28
+ const MAX_SEGMENTS = 6
29
+ const MAX_ID_LENGTH = 255
30
+
31
+ /** The Git blob id of a text: sha1 of `blob <byteLength>\0` and the UTF-8 bytes. Core does the same. */
32
+ export function gitBlobSha(text: string): string {
33
+ const bytes = Buffer.from(text, 'utf8')
34
+ return createHash('sha1')
35
+ .update(`blob ${bytes.length}\0`)
36
+ .update(bytes)
37
+ .digest('hex')
38
+ }
39
+
40
+ /**
41
+ * The version of a file text for a storage.
42
+ *
43
+ * - `github`: the store reports the Git blob id. Git keeps LF in the blob, so a CRLF working file
44
+ * (Windows with `core.autocrlf`) is hashed with LF. A file that really has CRLF in the repository
45
+ * looks changed at run time. That is safe: the page then comes from the store.
46
+ * - `fs` (and `memory`): the store hashes the file as it is on the disk, so the manifest does too.
47
+ */
48
+ export function versionOf(text: string, storage: string): string {
49
+ return gitBlobSha(storage === 'github' ? text.replace(/\r\n/g, '\n') : text)
50
+ }
51
+
52
+ /** Whether a content id is one that the CMS lists: 1 to 6 safe segments, and not excluded. */
53
+ export function isListedId(
54
+ id: string,
55
+ exclude: readonly string[] = []
56
+ ): boolean {
57
+ if (id.length === 0 || id.length > MAX_ID_LENGTH) {
58
+ return false
59
+ }
60
+ const segments = id.split('/')
61
+ if (
62
+ segments.length > MAX_SEGMENTS ||
63
+ segments.some((segment) => !SEGMENT_PATTERN.test(segment))
64
+ ) {
65
+ return false
66
+ }
67
+ return !exclude.some((entry) =>
68
+ entry.endsWith('/') ? id.startsWith(entry) : id === entry
69
+ )
70
+ }
71
+
72
+ /** One file in memory: the path is relative to the type folder, with `/`. */
73
+ export interface ManifestFile {
74
+ path: string
75
+ text: string
76
+ }
77
+
78
+ /** Builds the manifest of one type from its files. A file that is not a listed `.md` page is skipped. */
79
+ export function buildTypeManifest(
80
+ files: ManifestFile[],
81
+ options: { storage: string; exclude?: readonly string[] }
82
+ ): TypeManifest {
83
+ const manifest: TypeManifest = {}
84
+ for (const file of [...files].sort((a, b) =>
85
+ a.path < b.path ? -1 : a.path > b.path ? 1 : 0
86
+ )) {
87
+ if (!file.path.endsWith('.md')) {
88
+ continue
89
+ }
90
+ const id = file.path.slice(0, -'.md'.length)
91
+ if (!isListedId(id, options.exclude)) {
92
+ continue
93
+ }
94
+ manifest[id] = versionOf(file.text, options.storage)
95
+ }
96
+ return manifest
97
+ }
98
+
99
+ function walkMarkdown(dir: string, out: string[]): void {
100
+ let entries
101
+ try {
102
+ entries = readdirSync(dir, { withFileTypes: true })
103
+ } catch {
104
+ return
105
+ }
106
+ for (const entry of entries) {
107
+ if (entry.name.startsWith('.')) {
108
+ continue
109
+ }
110
+ const full = join(dir, entry.name)
111
+ if (entry.isDirectory()) {
112
+ if (!SKIPPED_DIRS.has(entry.name)) {
113
+ walkMarkdown(full, out)
114
+ }
115
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
116
+ out.push(full)
117
+ }
118
+ }
119
+ }
120
+
121
+ /** Reads the `.md` files under a type folder on the disk. A missing folder gives `[]`. */
122
+ export function readTypeFiles(folder: string): ManifestFile[] {
123
+ const found: string[] = []
124
+ walkMarkdown(folder, found)
125
+ return found.map((full) => ({
126
+ path: relative(folder, full).split(sep).join('/'),
127
+ text: readFileSync(full, 'utf8'),
128
+ }))
129
+ }
130
+
131
+ export interface ManifestTypeInput {
132
+ name: string
133
+ /** The folder under the CMS content dir. `''` is the content dir itself. */
134
+ source: string
135
+ exclude?: string[]
136
+ }
137
+
138
+ /** Builds the manifest of every type. `contentDir` is the absolute path of the CMS content dir. */
139
+ export function buildContentManifest(
140
+ types: ManifestTypeInput[],
141
+ options: { contentDir: string; storage: string }
142
+ ): ContentManifest {
143
+ const manifest: ContentManifest = {}
144
+ for (const type of types) {
145
+ const folder = type.source
146
+ ? join(options.contentDir, type.source)
147
+ : options.contentDir
148
+ manifest[type.name] = buildTypeManifest(readTypeFiles(folder), {
149
+ storage: options.storage,
150
+ exclude: type.exclude,
151
+ })
152
+ }
153
+ return manifest
154
+ }
155
+
156
+ /** The source of the virtual module that the server imports. */
157
+ export function manifestModuleSource(manifest: ContentManifest): string {
158
+ return `export default ${JSON.stringify(manifest)}\n`
159
+ }
@@ -0,0 +1,224 @@
1
+ /** One content type that the layer builds from a preset. */
2
+ export interface PlutoDocsTypeOptions {
3
+ /** The preset that makes the type. Only `docs` exists. */
4
+ preset: 'docs'
5
+ /** The content type name. It is a URL segment: `/admin/content/<name>`. */
6
+ name: string
7
+ /**
8
+ * The folder of the pages, under the CMS content dir. Use `''` for the content
9
+ * dir itself (the usual case for a Nuxt Content site).
10
+ */
11
+ source: string
12
+ label?: string
13
+ labelPlural?: string
14
+ /** The Iconify name of the type in the admin (sidebar, New menu, dashboard). Default `lucide:book-open`. */
15
+ icon?: string
16
+ /** Ids that the type must not list or edit. An id (`index`), or a folder with a trailing slash (`drafts/`). */
17
+ exclude?: string[]
18
+ /** The route prefix of the pages on the public site, for the View link of the form. Default: none. */
19
+ routePrefix?: string
20
+ /**
21
+ * The nav group of the type in the admin sidebar and the admin bar. Default `'pages'` (core's
22
+ * group). A string is a group id. `false` gives the type its own sidebar item.
23
+ */
24
+ navGroup?: string | false
25
+ }
26
+
27
+ /** How the public pages get their content. See the docs-setup skill. */
28
+ export type PlutoDocsRender = 'hybrid' | 'build' | 'runtime'
29
+
30
+ export interface PlutoDocsOptions {
31
+ /** The content types to register. Default: none. */
32
+ types?: PlutoDocsTypeOptions[]
33
+ /**
34
+ * `'hybrid'` (default): the pages are built with the site, and the server reads from the content
35
+ * repository the pages that changed after the build. `'build'`: Nuxt Content builds the pages
36
+ * and nothing else. `'runtime'`: the server reads every page from the repository at request time.
37
+ */
38
+ render?: PlutoDocsRender
39
+ /**
40
+ * Seconds that a CDN may cache the answers of the layer routes (`hybrid` and `runtime`). Default 10.
41
+ * It is short, so a save shows soon. `0` turns the cache header into `no-store`.
42
+ */
43
+ runtimeMaxAge?: number
44
+ }
45
+
46
+ export interface PlutoDocsResolveContext {
47
+ /** False when the app sets `plutoGithub.enabled` to false. */
48
+ cmsEnabled: boolean
49
+ }
50
+
51
+ export interface PlutoDocsResolveResult {
52
+ enabled: boolean
53
+ render: PlutoDocsRender
54
+ runtimeMaxAge: number
55
+ types: PlutoDocsTypeOptions[]
56
+ errors: string[]
57
+ }
58
+
59
+ export const PLUTO_DOCS_PRESETS = ['docs'] as const
60
+ export const PLUTO_DOCS_RENDER_MODES = ['hybrid', 'build', 'runtime'] as const
61
+ export const DEFAULT_RUNTIME_MAX_AGE = 10
62
+
63
+ export const CMS_OFF_ERROR =
64
+ 'The docs layer needs the standalone CMS. Remove plutoGithub.enabled: false, or remove the layer.'
65
+
66
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/
67
+ const SOURCE_SEGMENT = /^(?:\d+\.)?[a-z0-9]+(?:-[a-z0-9]+)*$/
68
+
69
+ function isRecord(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
71
+ }
72
+
73
+ /** Whether `source` is `''` or a safe nested path (the same segment rule as the CMS). */
74
+ export function isValidSource(source: unknown): source is string {
75
+ if (typeof source !== 'string') {
76
+ return false
77
+ }
78
+ if (source === '') {
79
+ return true
80
+ }
81
+ const segments = source.split('/')
82
+ return (
83
+ segments.length <= 6 &&
84
+ segments.every((segment) => SOURCE_SEGMENT.test(segment))
85
+ )
86
+ }
87
+
88
+ /** Normalizes a route prefix to `/a/b`, or `''` for none. */
89
+ export function normalizeRoutePrefix(prefix: unknown): string {
90
+ if (typeof prefix !== 'string') {
91
+ return ''
92
+ }
93
+ const segments = prefix.split('/').filter(Boolean)
94
+ return segments.length > 0 ? `/${segments.join('/')}` : ''
95
+ }
96
+
97
+ /**
98
+ * Checks the module options. A type with a problem is dropped and gives one
99
+ * error. When the CMS is off, the layer is off and gives one error. This file
100
+ * has no Nuxt import, so plain Vitest tests it.
101
+ */
102
+ export function resolvePlutoDocsOptions(
103
+ options: PlutoDocsOptions,
104
+ ctx: PlutoDocsResolveContext
105
+ ): PlutoDocsResolveResult {
106
+ const errors: string[] = []
107
+
108
+ if (!ctx.cmsEnabled) {
109
+ return {
110
+ enabled: false,
111
+ render: 'build',
112
+ runtimeMaxAge: DEFAULT_RUNTIME_MAX_AGE,
113
+ types: [],
114
+ errors: [CMS_OFF_ERROR],
115
+ }
116
+ }
117
+
118
+ let render: PlutoDocsRender = 'hybrid'
119
+ if (options.render !== undefined) {
120
+ if (
121
+ (PLUTO_DOCS_RENDER_MODES as readonly unknown[]).includes(options.render)
122
+ ) {
123
+ render = options.render
124
+ } else {
125
+ errors.push(
126
+ `plutoDocs.render must be one of: ${PLUTO_DOCS_RENDER_MODES.join(', ')}. Using "hybrid".`
127
+ )
128
+ }
129
+ }
130
+
131
+ let runtimeMaxAge = DEFAULT_RUNTIME_MAX_AGE
132
+ if (options.runtimeMaxAge !== undefined) {
133
+ if (
134
+ typeof options.runtimeMaxAge === 'number' &&
135
+ Number.isInteger(options.runtimeMaxAge) &&
136
+ options.runtimeMaxAge >= 0
137
+ ) {
138
+ runtimeMaxAge = options.runtimeMaxAge
139
+ } else {
140
+ errors.push(
141
+ `plutoDocs.runtimeMaxAge must be a whole number of seconds, 0 or more. Using ${DEFAULT_RUNTIME_MAX_AGE}.`
142
+ )
143
+ }
144
+ }
145
+
146
+ const types: PlutoDocsTypeOptions[] = []
147
+ const names = new Set<string>()
148
+ const entries: unknown[] = Array.isArray(options.types) ? options.types : []
149
+
150
+ for (const [index, entry] of entries.entries()) {
151
+ const where = `plutoDocs.types[${index}]`
152
+ if (!isRecord(entry)) {
153
+ errors.push(`${where} must be an object.`)
154
+ continue
155
+ }
156
+ if (!(PLUTO_DOCS_PRESETS as readonly unknown[]).includes(entry.preset)) {
157
+ errors.push(
158
+ `${where}.preset must be one of: ${PLUTO_DOCS_PRESETS.join(', ')}.`
159
+ )
160
+ continue
161
+ }
162
+ if (typeof entry.name !== 'string' || !NAME_PATTERN.test(entry.name)) {
163
+ errors.push(
164
+ `${where}.name must use lowercase letters, numbers and dashes, and start with a letter.`
165
+ )
166
+ continue
167
+ }
168
+ if (names.has(entry.name)) {
169
+ errors.push(`${where}.name "${entry.name}" is used twice.`)
170
+ continue
171
+ }
172
+ if (!isValidSource(entry.source)) {
173
+ errors.push(
174
+ `${where}.source must be '' or a folder path such as "docs" or "1.docs/api".`
175
+ )
176
+ continue
177
+ }
178
+ if (
179
+ entry.exclude !== undefined &&
180
+ !(
181
+ Array.isArray(entry.exclude) &&
182
+ entry.exclude.every((item) => typeof item === 'string')
183
+ )
184
+ ) {
185
+ errors.push(`${where}.exclude must be a list of strings.`)
186
+ continue
187
+ }
188
+ if (
189
+ entry.navGroup !== undefined &&
190
+ entry.navGroup !== false &&
191
+ !(typeof entry.navGroup === 'string' && entry.navGroup !== '')
192
+ ) {
193
+ errors.push(`${where}.navGroup must be a group id, or false.`)
194
+ continue
195
+ }
196
+ if (
197
+ entry.icon !== undefined &&
198
+ !(typeof entry.icon === 'string' && entry.icon !== '')
199
+ ) {
200
+ errors.push(`${where}.icon must be an icon name.`)
201
+ continue
202
+ }
203
+ names.add(entry.name)
204
+ types.push({
205
+ preset: 'docs',
206
+ name: entry.name,
207
+ source: entry.source,
208
+ ...(typeof entry.label === 'string' && { label: entry.label }),
209
+ ...(typeof entry.labelPlural === 'string' && {
210
+ labelPlural: entry.labelPlural,
211
+ }),
212
+ ...(typeof entry.icon === 'string' && { icon: entry.icon }),
213
+ ...(Array.isArray(entry.exclude) && {
214
+ exclude: entry.exclude as string[],
215
+ }),
216
+ routePrefix: normalizeRoutePrefix(entry.routePrefix),
217
+ ...((typeof entry.navGroup === 'string' || entry.navGroup === false) && {
218
+ navGroup: entry.navGroup,
219
+ }),
220
+ })
221
+ }
222
+
223
+ return { enabled: true, render, runtimeMaxAge, types, errors }
224
+ }
package/nuxt.config.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Point PLUTO_CORE_PATH at a local checkout (e.g. `../pluto/packages/core`) to test
2
+ // unpublished changes; unset, it resolves to the published npm package.
3
+ const coreLayer = process.env.PLUTO_CORE_PATH || '@plutocms/core'
4
+
5
+ export default defineNuxtConfig({
6
+ extends: [coreLayer],
7
+
8
+ modules: ['@nuxt/content'],
9
+
10
+ $meta: {
11
+ name: 'docs',
12
+ },
13
+
14
+ // Nuxt Content stores its data in SQLite. The native connector needs Node 22.5
15
+ // or newer and no extra package. An app can set another connector.
16
+ content: {
17
+ experimental: {
18
+ sqliteConnector: 'native',
19
+ },
20
+ },
21
+
22
+ // No `css` entry: the app owns the one Tailwind build, and Nuxt UI scans
23
+ // this layer's app/ dir. The tokens of this layer are in
24
+ // app/assets/css/pluto-layer.css. See the single-tailwind-build skill in pluto.
25
+
26
+ // The pages are Markdown files, so the fixed blog post type is off. A docs
27
+ // edit must redeploy the site, so the commit message has no `[skip ci]`
28
+ // marker by default. An app can change both. The other `plutoGithub` options
29
+ // have defaults in core (see the github-cms skill in pluto).
30
+ plutoGithub: {
31
+ postType: false,
32
+ commit: {
33
+ skipDeploy: false,
34
+ },
35
+ },
36
+ })
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@plutocms/docs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "type": "module",
4
+ "version": "0.15.0",
5
+ "description": "Docs layer for Pluto: edit the Markdown pages of a Nuxt Content site in the Pluto admin",
6
+ "author": "Victor Ribeiro",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/plutocms/docs#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/plutocms/docs.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/plutocms/docs/issues"
15
+ },
16
+ "main": "./nuxt.config.ts",
17
+ "scripts": {
18
+ "prepare": "nuxt prepare",
19
+ "prettier": "prettier --write .",
20
+ "lint": "eslint .",
21
+ "lint:fix": "eslint . --fix",
22
+ "typecheck": "nuxi typecheck",
23
+ "test": "vitest run"
24
+ },
25
+ "dependencies": {
26
+ "@nuxt/content": "^3.16.2",
27
+ "@nuxtjs/mdc": "^0.23.2",
28
+ "@plutocms/core": ">=0.39.0 <1.0.0",
29
+ "@types/sortablejs": "^1.15.9",
30
+ "reka-ui": "^2.11.0",
31
+ "remark-gfm": "4.0.1",
32
+ "remark-mdc": "3.11.1",
33
+ "remark-parse": "11.0.0",
34
+ "sortablejs": "^1.15.7",
35
+ "unified": "11.0.5",
36
+ "yaml": "2.9.1"
37
+ },
38
+ "devDependencies": {
39
+ "@antfu/eslint-config": "^9.5.1",
40
+ "@nuxt/eslint-config": "^1.17.0",
41
+ "@nuxt/ui": "^4.12.0",
42
+ "@types/node": "^26.6.5",
43
+ "@vueuse/nuxt": "^15.0.0",
44
+ "eslint": "^10.12.0",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-plugin-better-tailwindcss": "^4.10.0",
47
+ "nuxt": "^4.6.1",
48
+ "prettier": "^3.9.10",
49
+ "vitest": "^5.0.5",
50
+ "vue": "3.5.44",
51
+ "vue-router": "5.4.0",
52
+ "vue-tsc": "^3.3.13"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }
@@ -0,0 +1,7 @@
1
+ import { defineEventHandler } from 'h3'
2
+
3
+ // Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
4
+ // public API (`/api/pluto/v1/docs/:type/navigation`): the answer is `{ data }`, like core's routes.
5
+ export default defineEventHandler(async (event) => ({
6
+ data: await plutoRuntimeNavigation(event),
7
+ }))
@@ -0,0 +1,7 @@
1
+ import { defineEventHandler } from 'h3'
2
+
3
+ // Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
4
+ // public API (`/api/pluto/v1/docs/:type/page`): the answer is `{ data }`, like core's routes.
5
+ export default defineEventHandler(async (event) => ({
6
+ data: await plutoRuntimePage(event),
7
+ }))
@@ -0,0 +1,7 @@
1
+ import { defineEventHandler } from 'h3'
2
+
3
+ // Added by the module only when `plutoDocs.render` is `'hybrid'` or `'runtime'`. A route of the
4
+ // public API (`/api/pluto/v1/docs/:type/surround`): the answer is `{ data }`, like core's routes.
5
+ export default defineEventHandler(async (event) => ({
6
+ data: await plutoRuntimeSurround(event),
7
+ }))
@@ -0,0 +1,25 @@
1
+ import type { PlutoDocsTypeOptions } from '../../modules/pluto-docs/options'
2
+
3
+ // Registers the content types of the layer with the core content registry. The
4
+ // CMS Nitro plugin registers the adapter. The types are looked up per request,
5
+ // so the order of the two plugins does not matter. When the layer is off (the
6
+ // CMS is off), the module sets no config and this plugin does nothing.
7
+ export default defineNitroPlugin(() => {
8
+ const config = useRuntimeConfig().public.plutoDocs as
9
+ | {
10
+ types: PlutoDocsTypeOptions[]
11
+ render?: string
12
+ }
13
+ | undefined
14
+ if (!config) {
15
+ return
16
+ }
17
+ for (const entry of config.types) {
18
+ registerContentType(
19
+ createDocsType({
20
+ ...entry,
21
+ liveOnSave: config.render === 'hybrid' || config.render === 'runtime',
22
+ })
23
+ )
24
+ }
25
+ })