Skip to content

Commit 340fdf4

Browse files
authored
feat: auto generate markdown version of documents (#3688)
1 parent 6e4bf6f commit 340fdf4

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

docs/nuxt.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export default defineNuxtConfig({
5353
title: 'Complete Documentation',
5454
description: 'The complete documentation including all content',
5555
},
56+
contentRawMD: {
57+
excludeCollections: ['landing'],
58+
},
5659
},
5760
studio: {
5861
route: '/admin',

src/features/llms/module.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import defu from 'defu'
2-
import { createResolver, defineNuxtModule, addTypeTemplate, addServerPlugin } from '@nuxt/kit'
2+
import { createResolver, defineNuxtModule, addTypeTemplate, addServerPlugin, addServerHandler } from '@nuxt/kit'
33

44
export default defineNuxtModule({
55
meta: {
@@ -10,13 +10,36 @@ export default defineNuxtModule({
1010
const { resolve } = createResolver(import.meta.url)
1111

1212
addServerPlugin(resolve('runtime/server/content-llms.plugin'))
13+
if ((nuxt.options as unknown as { llms: { contentRawMD: false | { excludeCollections: string[] } } })?.llms?.contentRawMD !== false) {
14+
addServerHandler({ route: '/raw/**:slug.md', handler: resolve('runtime/server/routes/raw/[...slug].md.get') })
15+
}
16+
17+
nuxt.hook('modules:done', () => {
18+
// @ts-expect-error -- TODO: fix types
19+
nuxt.options.llms ||= {}
20+
// @ts-expect-error -- TODO: fix types
21+
nuxt.options.llms.contentRawMD = defu(nuxt.options.llms.contentRawMD, {
22+
excludeCollections: [],
23+
})
24+
25+
nuxt.options.runtimeConfig.llms ||= {}
26+
// @ts-expect-error -- TODO: fix types
27+
nuxt.options.runtimeConfig.llms.contentRawMD = defu(nuxt.options.llms.contentRawMD, {
28+
excludeCollections: [],
29+
})
30+
})
1331

1432
const typeTemplate = addTypeTemplate({
1533
filename: 'content/llms.d.ts' as `${string}.d.ts`,
1634
getContents: () => {
1735
return `
1836
import type { SQLOperator, PageCollections, PageCollectionItemBase } from '@nuxt/content'
1937
declare module 'nuxt-llms' {
38+
interface ModuleOptions {
39+
contentRawMD?: false | {
40+
excludeCollections?: string[]
41+
}
42+
}
2043
interface LLMsSection {
2144
contentCollection?: keyof PageCollections
2245
contentFilters?: Array<{

src/features/llms/runtime/server/content-llms.plugin.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import { appendHeader } from 'h3'
12
import { withBase } from 'ufo'
23
import type { NitroApp } from 'nitropack/types'
34
import type { ContentLLMSCollectionSection } from './utils'
45
import { createDocumentGenerator, prepareContentSections } from './utils'
56
import type { PageCollectionItemBase } from '@nuxt/content'
67
// @ts-expect-error - typecheck does not detect defineNitroPlugin in imports
78
import { defineNitroPlugin, queryCollection } from '#imports'
9+
import type { ModuleOptions } from 'nuxt-llms'
810

911
export default defineNitroPlugin((nitroApp: NitroApp) => {
12+
const prerenderPaths = new Set<string>()
1013
nitroApp.hooks.hook('llms:generate', async (event, options) => {
1114
prepareContentSections(options.sections)
1215

@@ -38,7 +41,7 @@ export default defineNitroPlugin((nitroApp: NitroApp) => {
3841
return {
3942
title: doc.title || doc?.seo?.title || '',
4043
description: doc.description || doc?.seo?.description || '',
41-
href: withBase(doc.path, options.domain),
44+
href: getDocumentLink(doc.path, section.contentCollection!, options),
4245
}
4346
}))
4447
}
@@ -78,4 +81,29 @@ export default defineNitroPlugin((nitroApp: NitroApp) => {
7881
}
7982
}
8083
})
84+
85+
if (['nitro-prerender', 'nitro-dev'].includes(import.meta.preset as string)) {
86+
nitroApp.hooks.hook('beforeResponse', (event) => {
87+
if (event.path === '/') {
88+
appendHeader(event, 'x-nitro-prerender', Array.from(prerenderPaths))
89+
}
90+
})
91+
}
92+
93+
function getDocumentLink(link: string, collection: string, options: ModuleOptions) {
94+
const contentRawMD = (options as unknown as { contentRawMD: false | { excludeCollections: string[] } })?.contentRawMD
95+
if (contentRawMD === false || contentRawMD?.excludeCollections?.includes(collection)) {
96+
return withBase(link, options.domain)
97+
}
98+
99+
link = `/raw${link}.md`
100+
101+
if (link.endsWith('/.md')) {
102+
link = link.slice(0, -3) + 'index.md'
103+
}
104+
105+
prerenderPaths.add(link)
106+
107+
return withBase(link, options.domain)
108+
}
81109
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { withLeadingSlash } from 'ufo'
2+
import { stringify } from 'minimark/stringify'
3+
import { queryCollection } from '@nuxt/content/server'
4+
import type { Collections, PageCollectionItemBase, ResolvedCollection } from '@nuxt/content'
5+
import { getRouterParams, eventHandler, createError, setHeader } from 'h3'
6+
import { useRuntimeConfig } from '#imports'
7+
import collections from '#content/manifest'
8+
9+
export default eventHandler(async (event) => {
10+
const config = useRuntimeConfig(event)
11+
const llmsConfig = config.llms as { contentRawMD: false | { excludeCollections: string[] } }
12+
const slug = getRouterParams(event)['slug.md']
13+
if (!slug?.endsWith('.md') || llmsConfig?.contentRawMD === false) {
14+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
15+
}
16+
17+
let path = withLeadingSlash(slug.replace('.md', ''))
18+
if (path.endsWith('/index')) {
19+
path = path.substring(0, path.length - 6)
20+
}
21+
22+
const excludeCollections = llmsConfig?.contentRawMD?.excludeCollections || []
23+
const _collections = Object.entries(collections as Record<string, ResolvedCollection>)
24+
.filter(([_key, value]) => value.type === 'page' && !excludeCollections.includes(_key))
25+
.map(([key]) => key) as string[]
26+
27+
let page: PageCollectionItemBase | null = null
28+
for (const collection of _collections) {
29+
page = await queryCollection(event, collection as keyof Collections).path(path).first() as PageCollectionItemBase | null
30+
if (page) {
31+
break
32+
}
33+
}
34+
35+
if (!page) {
36+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
37+
}
38+
39+
// Add title and description to the top of the page if missing
40+
if (page.body.value[0]?.[0] !== 'h1') {
41+
page.body.value.unshift(['blockquote', {}, page.description])
42+
page.body.value.unshift(['h1', {}, page.title])
43+
}
44+
45+
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
46+
return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
47+
})

0 commit comments

Comments
 (0)