@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,504 @@
1
+ import type { MDCParserResult } from '@nuxtjs/mdc'
2
+ import type { H3Event } from 'h3'
3
+ import type {
4
+ ManifestDiff,
5
+ RuntimeFolderConfig,
6
+ RuntimeNavItem,
7
+ RuntimeNavPage,
8
+ RuntimeSurroundItem,
9
+ } from '../../shared/utils/runtime-content'
10
+ import { createHash } from 'node:crypto'
11
+ import {
12
+ queryCollection,
13
+ queryCollectionItemSurroundings,
14
+ queryCollectionNavigation,
15
+ } from '@nuxt/content/server'
16
+ import { parseMarkdown } from '@nuxtjs/mdc/runtime'
17
+ import { getQuery, getRouterParam, setResponseHeader } from 'h3'
18
+ // The build manifest. The module makes this virtual module at build time. See modules/pluto-docs/manifest.ts.
19
+ import manifests from '#pluto-docs/manifest'
20
+ import { parseNavigationConfig } from '../../shared/utils/navigation-config'
21
+ import {
22
+ diffManifest,
23
+ frontmatterExtras,
24
+ } from '../../shared/utils/runtime-content'
25
+
26
+ // The server side of the `hybrid` and `runtime` render modes. The module adds the routes
27
+ // only in these modes. The logic is here, in plain functions, and the handlers in
28
+ // `server/handlers/` only call it. See the docs-setup skill.
29
+ //
30
+ // `runtime`: every answer comes from the content adapter that core registered (the GitHub
31
+ // CMS store with its cache).
32
+ // `hybrid`: the answer comes from the build data (Nuxt Content, `queryCollection` on the
33
+ // server) while the store has the same files as the build manifest. A changed or a new
34
+ // page comes from the store. When any file differs, the navigation and the surround come
35
+ // from the store. A removed page is a 404. A store error gives the build data.
36
+
37
+ const STALE_WHILE_REVALIDATE = 300
38
+ const PARSED_CACHE_SIZE = 200
39
+ const SNAPSHOT_CACHE_SIZE = 20
40
+ const COLLECTION_PATTERN = /^[A-Z][\w-]{0,63}$/i
41
+ const NAVIGATION_FILE = '.navigation.yml'
42
+
43
+ /** The page that the page route sends. The shape follows a Nuxt Content page. */
44
+ export interface PlutoRuntimePage {
45
+ id: string
46
+ stem: string
47
+ extension: 'md'
48
+ path: string
49
+ title: string
50
+ description: string
51
+ seo: { title?: string; description?: string; [key: string]: unknown }
52
+ navigation?: false | { title?: string; icon?: string; [key: string]: unknown }
53
+ meta: Record<string, unknown>
54
+ body: MDCParserResult['body'] & { toc?: MDCParserResult['toc'] }
55
+ /** The other frontmatter keys of the file, at the top level, like the schema keys of Nuxt Content. */
56
+ [key: string]: unknown
57
+ }
58
+
59
+ interface RuntimeContext {
60
+ type: PlutoContentType
61
+ prefix: string
62
+ mode: 'hybrid' | 'runtime'
63
+ /** The Nuxt Content collection for the build data (hybrid). */
64
+ collection: string | undefined
65
+ }
66
+
67
+ /** The public config of the layer. The site may type its runtime config in its own way, so the code reads it through this type. */
68
+ interface PublicConfig {
69
+ render: string
70
+ runtimeMaxAge?: number
71
+ types: Array<{ name: string; routePrefix?: string }>
72
+ }
73
+
74
+ type Manifests = Record<string, Record<string, string>> | undefined
75
+
76
+ function notFound(): Error {
77
+ return createPlutoError({
78
+ statusCode: 404,
79
+ message: 'Not found.',
80
+ key: 'pluto.errors.notFound',
81
+ })
82
+ }
83
+
84
+ function queryText(event: H3Event, key: string): string {
85
+ const value = getQuery(event)[key]
86
+ return typeof value === 'string' ? value : ''
87
+ }
88
+
89
+ /**
90
+ * Checks the request and gives the type. A public read is always anonymous: the session
91
+ * cookie is never read, so every visitor gets the same answer and a CDN may cache it.
92
+ */
93
+ function runtimeContext(event: H3Event): RuntimeContext {
94
+ markPublicRead(event)
95
+ const config = useRuntimeConfig(event).public.plutoDocs as unknown as
96
+ PublicConfig | undefined
97
+ const name = getRouterParam(event, 'type') ?? ''
98
+ const entry =
99
+ config?.render === 'hybrid' || config?.render === 'runtime'
100
+ ? config.types.find((type) => type.name === name)
101
+ : undefined
102
+ const type = entry ? getContentType(name) : undefined
103
+ if (!config || !entry || !type) {
104
+ throw notFound()
105
+ }
106
+ const collection = queryText(event, 'collection')
107
+ return {
108
+ type,
109
+ prefix: entry.routePrefix ?? '',
110
+ mode: config.render as 'hybrid' | 'runtime',
111
+ collection: COLLECTION_PATTERN.test(collection) ? collection : undefined,
112
+ }
113
+ }
114
+
115
+ function setCacheHeaders(event: H3Event): void {
116
+ const configured = Number(
117
+ (
118
+ useRuntimeConfig(event).public.plutoDocs as unknown as
119
+ PublicConfig | undefined
120
+ )?.runtimeMaxAge
121
+ )
122
+ const maxAge =
123
+ Number.isFinite(configured) && configured >= 0 ? configured : 10
124
+ const swr = Math.min(STALE_WHILE_REVALIDATE, Math.max(maxAge * 3, 1))
125
+ setResponseHeader(
126
+ event,
127
+ 'Cache-Control',
128
+ maxAge === 0
129
+ ? 'no-store'
130
+ : `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}`
131
+ )
132
+ }
133
+
134
+ function text(value: unknown): string {
135
+ return typeof value === 'string' ? value : ''
136
+ }
137
+
138
+ function isRecord(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
140
+ }
141
+
142
+ /** Every page of the type, in tree order, without the body. */
143
+ async function listPages(
144
+ event: H3Event,
145
+ type: PlutoContentType
146
+ ): Promise<PlutoContentItem[]> {
147
+ const result = await requireContentAdapter().list(
148
+ { event, type },
149
+ { sort: type.defaultSort, includeUnpublished: false }
150
+ )
151
+ return result.data
152
+ }
153
+
154
+ function toNavPage(item: PlutoContentItem): RuntimeNavPage {
155
+ return {
156
+ id: String(item.id),
157
+ title: text(item.title),
158
+ description: text(item.description) || undefined,
159
+ navigationTitle: text(item.navigationTitle) || undefined,
160
+ navigationIcon: text(item.navigationIcon) || undefined,
161
+ navigationHidden: item.navigationHidden === true,
162
+ }
163
+ }
164
+
165
+ // --- The store state against the build manifest ------------------------------
166
+
167
+ interface Snapshot {
168
+ items: PlutoContentItem[]
169
+ ids: string[]
170
+ diff: ManifestDiff
171
+ }
172
+
173
+ /** Lists the store and compares it with the manifest. */
174
+ async function takeSnapshot(
175
+ event: H3Event,
176
+ type: PlutoContentType
177
+ ): Promise<Snapshot> {
178
+ const items = await listPages(event, type)
179
+ const manifest = (manifests as Manifests)?.[type.name]
180
+ const diff = diffManifest(
181
+ manifest,
182
+ items.map((item) => ({ id: String(item.id), version: item._version }))
183
+ )
184
+ return { items, ids: items.map((item) => String(item.id)), diff }
185
+ }
186
+
187
+ /**
188
+ * The snapshot, or `undefined` when the answer must come from the build data: the mode is
189
+ * hybrid and the store failed (no App, rate limit, outage). In `runtime` mode the error goes up.
190
+ */
191
+ async function snapshotOrFallback(
192
+ event: H3Event,
193
+ context: RuntimeContext
194
+ ): Promise<Snapshot | undefined> {
195
+ try {
196
+ return await takeSnapshot(event, context.type)
197
+ } catch (error) {
198
+ if (context.mode === 'hybrid') {
199
+ console.warn(
200
+ '[pluto-docs] The store failed. The build data answers.',
201
+ (error as Error).message
202
+ )
203
+ return undefined
204
+ }
205
+ throw error
206
+ }
207
+ }
208
+
209
+ // --- The parser --------------------------------------------------------------
210
+
211
+ type Highlighter =
212
+ NonNullable<
213
+ NonNullable<Parameters<typeof parseMarkdown>[1]>['highlight']
214
+ > extends infer H
215
+ ? H extends { highlighter?: infer F }
216
+ ? F
217
+ : never
218
+ : never
219
+
220
+ let highlighterPromise: Promise<Highlighter> | undefined
221
+
222
+ /**
223
+ * The code highlighter of `@nuxtjs/mdc`, made at build time with the themes and the
224
+ * languages of the site. The default highlighter of the parser asks the `/api/_mdc/highlight`
225
+ * route, which Nuxt Content turns off. So the parser gets this one.
226
+ */
227
+ function getHighlighter(): Promise<Highlighter> {
228
+ // eslint-disable-next-line ts/ban-ts-comment
229
+ // @ts-ignore The module is a Nitro alias of @nuxtjs/mdc. It has no type file.
230
+ highlighterPromise ??= import('#mdc-highlighter').then(
231
+ (module) => module.default as Highlighter
232
+ )
233
+ return highlighterPromise
234
+ }
235
+
236
+ // Parsing is the slow part. The cache is by blob id (`_version`), which never changes for
237
+ // one text, so an entry never goes stale. A new text has a new blob id.
238
+ const parsedCache = new Map<string, Promise<MDCParserResult>>()
239
+
240
+ function parseBody(
241
+ version: string | undefined,
242
+ body: string
243
+ ): Promise<MDCParserResult> {
244
+ const toc = (
245
+ useRuntimeConfig().plutoDocs as
246
+ { toc?: { depth?: number; searchDepth?: number } } | undefined
247
+ )?.toc
248
+ const parse = async () =>
249
+ parseMarkdown(body, {
250
+ ...(toc && { toc }),
251
+ highlight: { highlighter: await getHighlighter() },
252
+ })
253
+ if (!version) {
254
+ return parse()
255
+ }
256
+ const hit = parsedCache.get(version)
257
+ if (hit) {
258
+ return hit
259
+ }
260
+ const promise = parse()
261
+ parsedCache.set(version, promise)
262
+ promise.catch(() => parsedCache.delete(version))
263
+ if (parsedCache.size > PARSED_CACHE_SIZE) {
264
+ parsedCache.delete(parsedCache.keys().next().value as string)
265
+ }
266
+ return promise
267
+ }
268
+
269
+ // --- Folder configs (.navigation.yml) ----------------------------------------
270
+
271
+ /** The folders of the ids: every parent path, once (`a/b/c` gives `a` and `a/b`). */
272
+ function folderPaths(ids: string[]): string[] {
273
+ const folders = new Set<string>()
274
+ for (const id of ids) {
275
+ const parts = id.split('/').slice(0, -1)
276
+ parts.forEach((_part, index) =>
277
+ folders.add(parts.slice(0, index + 1).join('/'))
278
+ )
279
+ }
280
+ return [...folders]
281
+ }
282
+
283
+ const foldersCache = new Map<string, Record<string, RuntimeFolderConfig>>()
284
+
285
+ /**
286
+ * The `.navigation.yml` of each folder of the type, read through the adapter (cached by blob in
287
+ * core). The result is cached here by the listing (ids and versions), so a request with an
288
+ * unchanged listing reads no file. An adapter without `readFile` gives no configs.
289
+ */
290
+ async function readFolderConfigs(
291
+ event: H3Event,
292
+ type: PlutoContentType,
293
+ items: PlutoContentItem[]
294
+ ): Promise<Record<string, RuntimeFolderConfig>> {
295
+ const adapter = requireContentAdapter()
296
+ if (!adapter.readFile) {
297
+ return {}
298
+ }
299
+ const hash = createHash('sha1')
300
+ for (const item of items) {
301
+ hash.update(`${item.id}\0${item._version ?? ''}\n`)
302
+ }
303
+ const key = `${type.name}:${hash.digest('hex')}`
304
+ const hit = foldersCache.get(key)
305
+ if (hit) {
306
+ return hit
307
+ }
308
+ const configs: Record<string, RuntimeFolderConfig> = {}
309
+ await Promise.all(
310
+ folderPaths(items.map((item) => String(item.id))).map(async (folder) => {
311
+ const file = await adapter.readFile!(
312
+ { event, type },
313
+ `${folder}/${NAVIGATION_FILE}`
314
+ )
315
+ const config = file ? parseNavigationConfig(file.text) : undefined
316
+ if (config) {
317
+ configs[folder] = config
318
+ }
319
+ })
320
+ )
321
+ foldersCache.set(key, configs)
322
+ if (foldersCache.size > SNAPSHOT_CACHE_SIZE) {
323
+ foldersCache.delete(foldersCache.keys().next().value as string)
324
+ }
325
+ return configs
326
+ }
327
+
328
+ async function storeTree(
329
+ event: H3Event,
330
+ context: RuntimeContext,
331
+ items: PlutoContentItem[],
332
+ withDescription: boolean
333
+ ): Promise<RuntimeNavItem[]> {
334
+ const folders = await readFolderConfigs(event, context.type, items)
335
+ return buildNavigationTree(items.map(toNavPage), {
336
+ routePrefix: context.prefix,
337
+ includeRoot: queryText(event, 'includeRoot') === '1',
338
+ withDescription,
339
+ folders,
340
+ })
341
+ }
342
+
343
+ // --- The page ----------------------------------------------------------------
344
+
345
+ /** The page from the store: the text is parsed and the frontmatter passes whole. */
346
+ async function storePage(
347
+ event: H3Event,
348
+ context: RuntimeContext,
349
+ id: string
350
+ ): Promise<PlutoRuntimePage | null> {
351
+ const item = await requireContentAdapter().get(
352
+ { event, type: context.type },
353
+ id
354
+ )
355
+ if (!item) {
356
+ return null
357
+ }
358
+ const parsed = await parseBody(item._version, text(item.body))
359
+ const frontmatter = isRecord(item._frontmatter) ? item._frontmatter : {}
360
+ const extras = frontmatterExtras(frontmatter)
361
+
362
+ const title = text(item.title) || text(parsed.data?.title)
363
+ const description = text(item.description) || text(parsed.data?.description)
364
+ // Like Nuxt Content: the seo title and description default to the page ones.
365
+ const seo: PlutoRuntimePage['seo'] = {
366
+ ...(isRecord(frontmatter.seo) ? frontmatter.seo : {}),
367
+ title: text(item.seoTitle) || title,
368
+ description: text(item.seoDescription) || description,
369
+ }
370
+ const rawNavigation = frontmatter.navigation
371
+ const navigation: PlutoRuntimePage['navigation'] =
372
+ rawNavigation === false || item.navigationHidden === true
373
+ ? false
374
+ : isRecord(rawNavigation)
375
+ ? rawNavigation
376
+ : undefined
377
+
378
+ return {
379
+ ...extras.top,
380
+ id: `${context.type.name}:${id}.md`,
381
+ stem: id,
382
+ extension: 'md',
383
+ path: contentRoutePath(id, context.prefix),
384
+ title,
385
+ description,
386
+ seo,
387
+ ...(navigation !== undefined && { navigation }),
388
+ meta: extras.meta,
389
+ body: { ...parsed.body, toc: parsed.toc },
390
+ }
391
+ }
392
+
393
+ /** The page from the build data (Nuxt Content), or `null`. */
394
+ async function buildPage(
395
+ event: H3Event,
396
+ collection: string,
397
+ path: string
398
+ ): Promise<unknown> {
399
+ try {
400
+ return await queryCollection(event, collection as never)
401
+ .path(path)
402
+ .first()
403
+ } catch (error) {
404
+ console.warn(
405
+ '[pluto-docs] The build data failed.',
406
+ (error as Error).message
407
+ )
408
+ return null
409
+ }
410
+ }
411
+
412
+ /** `GET /api/pluto/v1/docs/:type/page?path=/a/b&collection=docs` */
413
+ export async function plutoRuntimePage(event: H3Event): Promise<unknown> {
414
+ const context = runtimeContext(event)
415
+ const path = queryText(event, 'path')
416
+ setCacheHeaders(event)
417
+
418
+ const snapshot = await snapshotOrFallback(event, context)
419
+ if (!snapshot) {
420
+ // The store failed in hybrid mode.
421
+ const page = context.collection
422
+ ? await buildPage(event, context.collection, path)
423
+ : null
424
+ if (!page) {
425
+ throw notFound()
426
+ }
427
+ return page
428
+ }
429
+
430
+ const id = pageIdForRoute(snapshot.ids, path, context.prefix)
431
+ if (!id) {
432
+ // Not in the store. A page of the manifest was removed. Another page may be outside the type.
433
+ if (context.mode === 'hybrid' && context.collection) {
434
+ const built = Object.keys(
435
+ (manifests as Manifests)?.[context.type.name] ?? {}
436
+ )
437
+ if (!pageIdForRoute(built, path, context.prefix)) {
438
+ const page = await buildPage(event, context.collection, path)
439
+ if (page) {
440
+ return page
441
+ }
442
+ }
443
+ }
444
+ throw notFound()
445
+ }
446
+
447
+ const stale =
448
+ snapshot.diff.changed.includes(id) || snapshot.diff.added.includes(id)
449
+ if (context.mode === 'hybrid' && !stale && context.collection) {
450
+ const page = await buildPage(event, context.collection, path)
451
+ if (page) {
452
+ return page
453
+ }
454
+ }
455
+ const page = await storePage(event, context, id)
456
+ if (!page) {
457
+ throw notFound()
458
+ }
459
+ return page
460
+ }
461
+
462
+ /** `GET /api/pluto/v1/docs/:type/navigation?collection=docs&includeRoot=1` */
463
+ export async function plutoRuntimeNavigation(event: H3Event): Promise<unknown> {
464
+ const context = runtimeContext(event)
465
+ setCacheHeaders(event)
466
+ const snapshot = await snapshotOrFallback(event, context)
467
+ if (
468
+ context.mode === 'hybrid' &&
469
+ context.collection &&
470
+ (!snapshot || snapshot.diff.same)
471
+ ) {
472
+ return await queryCollectionNavigation(event, context.collection as never)
473
+ }
474
+ if (!snapshot) {
475
+ throw notFound()
476
+ }
477
+ return await storeTree(event, context, snapshot.items, false)
478
+ }
479
+
480
+ /** `GET /api/pluto/v1/docs/:type/surround?path=/a/b&collection=docs&includeRoot=1` */
481
+ export async function plutoRuntimeSurround(
482
+ event: H3Event
483
+ ): Promise<Array<RuntimeSurroundItem | null> | unknown> {
484
+ const context = runtimeContext(event)
485
+ setCacheHeaders(event)
486
+ const path = queryText(event, 'path').replace(/(?<=.)\/+$/, '')
487
+ const snapshot = await snapshotOrFallback(event, context)
488
+ if (
489
+ context.mode === 'hybrid' &&
490
+ context.collection &&
491
+ (!snapshot || snapshot.diff.same)
492
+ ) {
493
+ return await queryCollectionItemSurroundings(
494
+ event,
495
+ context.collection as never,
496
+ path,
497
+ { fields: ['description'] as never }
498
+ )
499
+ }
500
+ if (!snapshot) {
501
+ throw notFound()
502
+ }
503
+ return surroundOf(await storeTree(event, context, snapshot.items, true), path)
504
+ }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ PlutoDocsOptions,
3
+ PlutoDocsRender,
4
+ PlutoDocsTypeOptions,
5
+ } from '../../modules/pluto-docs/options'
6
+ import type { PlutoBlocksConfig } from '../utils/blocks/types'
7
+
8
+ // The `pluto-docs` module fills the public key. See modules/pluto-docs/index.ts.
9
+ declare module '@nuxt/schema' {
10
+ interface NuxtConfig {
11
+ plutoDocs?: PlutoDocsOptions
12
+ }
13
+ interface NuxtOptions {
14
+ plutoDocs?: PlutoDocsOptions
15
+ }
16
+ interface PublicRuntimeConfig {
17
+ plutoDocs?: {
18
+ types: PlutoDocsTypeOptions[]
19
+ /** `'hybrid'`, `'build'` or `'runtime'`. The composables read it. */
20
+ render: PlutoDocsRender
21
+ /** Seconds of the public cache of the layer routes. */
22
+ runtimeMaxAge: number
23
+ }
24
+ }
25
+ interface RuntimeConfig {
26
+ plutoDocs?: {
27
+ /** The `toc` option of `content.build.markdown`, for the run-time parser. */
28
+ toc?: { depth?: number; searchDepth?: number }
29
+ }
30
+ }
31
+ // A site declares its blocks in app.config.ts. See shared/utils/blocks/registry.ts.
32
+ interface AppConfigInput {
33
+ plutoBlocks?: PlutoBlocksConfig
34
+ }
35
+ interface AppConfig {
36
+ plutoBlocks?: PlutoBlocksConfig
37
+ }
38
+ }
39
+
40
+ export {}
@@ -0,0 +1,6 @@
1
+ // The build manifest of the hybrid mode: type name → content id → version. The module makes it
2
+ // as a Nitro virtual module. See modules/pluto-docs/manifest.ts.
3
+ declare module '#pluto-docs/manifest' {
4
+ const manifest: Record<string, Record<string, string>>
5
+ export default manifest
6
+ }