Skip to content

Commit e6009a6

Browse files
committed
fix: llms plugin headers & remove slot
1 parent 56790c7 commit e6009a6

File tree

18 files changed

+563
-205
lines changed

18 files changed

+563
-205
lines changed

docs/vitepress-theme/index.md

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,12 @@ export default defineConfig(() => {
147147
repoURL: 'https://github.com/your-org/your-repo' // Can be different from main repoURL
148148
},
149149

150-
// Markdown generation (optional)
151-
// Set to false to disable automatic markdown generation
152-
// Or customize with options
153-
markdown: {
154-
cacheTTL: 3600000, // Cache duration in milliseconds (default: 1 hour)
155-
verbose: false // Enable verbose logging (default: false)
150+
// LLM-friendly markdown generation (optional)
151+
// Set to false to disable .md outputs
152+
// Or customize using vitepress-plugin-llms settings
153+
llms: {
154+
domain: 'https://docs.nimiq.com',
155+
injectLLMHint: false
156156
}
157157
})
158158
]
@@ -169,14 +169,14 @@ export default defineConfig(() => {
169169
| `repoURL` | `string` | - | GitHub repository URL used for source code links and as default for GitChangelog |
170170
| `contentPath` | `string` | `''` | Directory path where documentation files are located relative to repository root |
171171
| `gitChangelog` | `object \| false` | Uses `repoURL` | Git changelog configuration or `false` to disable |
172-
| `markdown` | `object \| boolean` | `true` | Markdown generation options or `false` to disable |
172+
| `llms` | `object \| boolean` | `true` | LLM-friendly markdown generation options or `false` to disable |
173173

174174
The plugin automatically:
175175

176176
- Configures Git changelog functionality using the provided repository URL
177177
- Enables source code viewing and copying features
178178
- Constructs proper URLs for "View Source" and "Edit Page" links
179-
- Generates markdown files for all HTML pages during build
179+
- Generates LLM-optimized markdown files for every page using `vitepress-plugin-llms`
180180

181181
### Register the theme as internal dependency
182182

@@ -206,14 +206,14 @@ For more information about why configure this, please refer to the [Server-Side
206206

207207
## Copy Page as Markdown
208208

209-
The theme automatically generates markdown versions of all HTML pages during build using the [@mdream/vite](https://github.com/harlan-zw/mdream) plugin. This enables users to easily copy page content as markdown for use with LLMs, note-taking apps, or other tools.
209+
The theme automatically generates markdown versions of all HTML pages during build using the [vitepress-plugin-llms](https://github.com/okineadev/vitepress-plugin-llms) toolkit. This enables users to easily copy page content as markdown for use with LLMs, note-taking apps, or other tools.
210210

211211
### How it works
212212

213-
1. **Automatic Generation**: The `NimiqVitepressVitePlugin` includes the mdream Vite plugin which automatically converts HTML pages to markdown during build
214-
2. **Development Support**: In dev mode, markdown is generated on-demand when accessing any page with `.md` extension (e.g., `/guide.html``/guide.md`)
213+
1. **Automatic Generation**: The `NimiqVitepressVitePlugin` bundles `vitepress-plugin-llms`, which converts each documentation page into a dedicated `.md` file during build
214+
2. **Development Support**: In dev mode, the plugin serves `.md` pages through the Vite dev server so you can copy or test locally without a full build
215215
3. **Copy Button**: Each page includes a "Copy page" button that fetches the generated markdown and copies it to the clipboard
216-
4. **Configurable**: You can customize caching, verbose logging, or disable the feature entirely through the plugin options
216+
4. **Configurable**: You can override any `llmstxt` settings (e.g., domain, hints, additional outputs) or disable the integration entirely through the plugin options
217217

218218
The markdown files are saved alongside their corresponding HTML files in the build output (e.g., `getting-started.html``getting-started.md`).
219219

@@ -224,12 +224,13 @@ You can configure the markdown generation or disable it entirely:
224224
```ts
225225
NimiqVitepressVitePlugin({
226226
// Disable markdown generation
227-
markdown: false,
227+
llms: false,
228228

229229
// Or customize options
230-
markdown: {
231-
cacheTTL: 7200000, // 2 hours cache
232-
verbose: true // Enable logging
230+
llms: {
231+
domain: 'https://docs.example.com',
232+
generateLLMsTxt: true, // Enable llms.txt sitemap
233+
generateLLMsFullTxt: false, // Keep the bundle light
233234
}
234235
})
235236
```
@@ -325,34 +326,6 @@ copyOptions: source-only # Show only "View Source" button
325326
---
326327
```
327328

328-
## Custom Header Slot
329-
330-
Add custom content between the search bar and modules dropdown in the header navigation.
331-
332-
```ts [.vitepress/theme/index.ts]
333-
import { defineNimiqThemeConfig } from 'nimiq-vitepress-theme/theme'
334-
import CustomHeaderNav from './CustomHeaderNav.vue'
335-
336-
export default defineNimiqThemeConfig({
337-
Layout() {
338-
return {
339-
'header-nav-before-modules': CustomHeaderNav
340-
}
341-
}
342-
})
343-
```
344-
345-
```vue [.vitepress/theme/CustomHeaderNav.vue]
346-
<template>
347-
<a href="https://example.com" target="_blank" flex="~ items-center gap-8" f-px-2xs py-4 f-rounded-xs bg="transparent hocus:neutral-200" transition-colors>
348-
<div i-tabler:external-link />
349-
<span f-text-sm font-medium>Docs</span>
350-
</a>
351-
</template>
352-
```
353-
354-
The slot appears in both desktop header and mobile sidebar (above the modules selector) in the home layout.
355-
356329
## Customization
357330

358331
This theme **has not been developed with customatization in mind**. In fact, it has the least possible amount of options on purpose as we want to keep it simple.

packages/nimiq-icons/src/flags/icons.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,7 @@
12681268
"body": "<mask id=\"nimiq-flags-zw-hexagon-pbk987\" width=\"32\" height=\"28\" x=\"0\" y=\"2\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"#fff\" d=\"M31.15 14.71 24.707 3.54a2.58 2.58 0 00-2.234-1.29H9.582c-.92 0-1.77.49-2.23 1.29L.907 14.71c-.46.8-.46 1.78 0 2.58l6.445 11.17c.46.8 1.31 1.29 2.23 1.29h12.89c.92 0 1.77-.49 2.23-1.29l6.445-11.17c.464-.8.464-1.78.004-2.58\"/></mask><g fill=\"none\"><g mask=\"url(#nimiq-flags-zw-hexagon-pbk987)\"><path fill=\"#6DA544\" d=\"M1.962 0H32v32H1.962z\"/><path fill=\"#FFDA44\" d=\"M3.613 4.581H32v4.582l-4.069 6.875 4.07 6.875v4.58H3.612z\"/><path fill=\"#D80027\" d=\"M8.25 9.162H32v4.582l-1.687 2.25L32 18.325v4.581H8.25z\"/><path fill=\"#EEE\" d=\"M0 0v32l17.488-16z\"/><path fill=\"#D80027\" d=\"m6.437 11.825 1.032 3.188h3.35l-2.713 1.975 1.038 3.187-2.713-1.969-2.712 1.969 1.037-3.187-2.712-1.976h3.35z\"/><path fill=\"#FFDA44\" d=\"m9.281 16.263-2.7-.957-.212-1.937a1.044 1.044 0 10-2.032.475l-.75.756h1.344c0 1.4-1.044 1.4-1.044 2.794l.575 1.387h3.482l.58-1.387q.086-.198.107-.413c.5-.2.65-.719.65-.719\"/><path fill=\"#333\" d=\"m1.963 0 13.75 13.75H32v4.575H15.638L1.962 32H0l16-16L0 0z\"/><path fill=\"url(#nimiq-flags-zw-hexagon-pbk987)\" d=\"M31.15 14.71 24.707 3.54a2.58 2.58 0 00-2.234-1.29H9.582c-.92 0-1.77.49-2.23 1.29L.907 14.71c-.46.8-.46 1.78 0 2.58l6.445 11.17c.46.8 1.31 1.29 2.23 1.29h12.89c.92 0 1.77-.49 2.23-1.29l6.445-11.17c.464-.8.464-1.78.004-2.58\"/></g><defs><radialGradient id=\"nimiq-flags-zw-hexagon-pbk987\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"matrix(30.943 0 0 30.9452 23.829 29.395)\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#1D1D1B\" stop-opacity=\".3\"/><stop offset=\"1\" stop-color=\"#E9B213\" stop-opacity=\"0\"/></radialGradient></defs></g>"
12691269
}
12701270
},
1271-
"lastModified": 1759222558,
1271+
"lastModified": 1759234944,
12721272
"width": 32,
12731273
"height": 32
12741274
}

packages/nimiq-icons/src/flags/info.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"prefix": "nimiq-flags",
33
"name": "nimiq-flags",
44
"total": 422,
5-
"version": "1.0.0-beta.105",
5+
"version": "1.0.0-beta.107",
66
"author": {
77
"name": "onmax",
88
"url": "https://github.com/onmax"

packages/nimiq-icons/src/flags/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "nimiq-flags",
33
"description": "The Nimiq Flags as an iconify icon set.",
44
"version": "1.0.0-beta.107",
5-
"iconSetVersion": "1.0.0-beta.105",
5+
"iconSetVersion": "1.0.0-beta.107",
66
"main": "index.js",
77
"module": "index.mjs",
88
"types": "index.d.ts",

packages/nimiq-icons/src/icons/icons.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1586,7 +1586,7 @@
15861586
"hidden": true
15871587
}
15881588
},
1589-
"lastModified": 1759222558,
1589+
"lastModified": 1759234944,
15901590
"width": 12,
15911591
"height": 12
15921592
}

packages/nimiq-icons/src/icons/info.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"prefix": "nimiq",
33
"name": "nimiq-icons",
44
"total": 84,
5-
"version": "1.0.0-beta.105",
5+
"version": "1.0.0-beta.107",
66
"author": {
77
"name": "onmax",
88
"url": "https://github.com/onmax"

packages/nimiq-icons/src/icons/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "nimiq-icons",
33
"description": "The Nimiq Icons as an iconify icon set.",
44
"version": "1.0.0-beta.107",
5-
"iconSetVersion": "1.0.0-beta.105",
5+
"iconSetVersion": "1.0.0-beta.107",
66
"main": "index.js",
77
"module": "index.mjs",
88
"types": "index.d.ts",

packages/nimiq-vitepress-theme/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"dependencies": {
7272
"@iconify-json/tabler": "catalog:icons",
7373
"@iconify-json/vscode-icons": "catalog:icons",
74-
"@mdream/vite": "^0.11.0",
7574
"@nolebase/vitepress-plugin-git-changelog": "catalog:build",
7675
"@vueuse/core": "catalog:utils",
7776
"@vueuse/motion": "^3.0.3",
@@ -81,6 +80,7 @@
8180
"nimiq-icons": "workspace:*",
8281
"reka-ui": "catalog:frontend",
8382
"vaul-vue": "catalog:prod",
83+
"vitepress-plugin-llms": "^1.7.5",
8484
"vue": "catalog:frontend",
8585
"vue-router": "catalog:frontend",
8686
"vue-sonner": "^1.1.4"

packages/nimiq-vitepress-theme/src/composables/useSourceCode.ts

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,136 @@
1-
import { useClipboard } from '@vueuse/core'
21
import { join } from 'pathe'
32
import { useData } from 'vitepress'
4-
import { computed, ref } from 'vue'
3+
import { computed, onBeforeUnmount, ref } from 'vue'
54
import { toast } from 'vue-sonner'
65
import { useChangelog } from './useChangelog'
76
import { useNimiqConfig } from './useNimiqConfig'
87

98
export function useSourceCode() {
10-
const { page, frontmatter } = useData()
9+
const { page, frontmatter, site } = useData()
1110
const { repoURL } = useChangelog()
1211
const nimiqConfig = useNimiqConfig()
1312

14-
// Use a safer approach for SSR compatibility
15-
const clipboardResult = typeof window !== 'undefined'
16-
? useClipboard()
17-
: { copy: async () => {}, copied: ref(false), isSupported: ref(false) }
18-
const { copy, copied, isSupported } = clipboardResult
13+
if (typeof window === 'undefined') {
14+
const fallbackCopied = ref(false)
15+
const fallbackSupported = ref(false)
16+
const emptyString = computed(() => '')
17+
const copyOptions = computed(() => ({
18+
markdownLink: false,
19+
viewMarkdown: false,
20+
chatgpt: false,
21+
claude: false,
22+
}))
23+
24+
return {
25+
showSourceCode: computed(() => false),
26+
showCopyMarkdown: computed(() => false),
27+
editUrl: emptyString,
28+
sourceCodeUrl: emptyString,
29+
sourceCodeLabel: computed(() => 'View Source'),
30+
copyMarkdownContent: async () => {},
31+
copied: fallbackCopied,
32+
isSupported: fallbackSupported,
33+
copyMarkdownLink: async () => {},
34+
chatGPTUrl: emptyString,
35+
claudeUrl: emptyString,
36+
viewAsMarkdown: () => {},
37+
copyOptionsConfig: copyOptions,
38+
hasDropdownOptions: computed(() => false),
39+
}
40+
}
41+
42+
const copied = ref(false)
43+
const isSupported = ref(false)
44+
let resetTimer: ReturnType<typeof setTimeout> | undefined
45+
46+
const determineSupport = () => {
47+
if (typeof window === 'undefined') {
48+
isSupported.value = false
49+
return
50+
}
51+
52+
const nav = window.navigator
53+
const hasClipboardAPI = !!nav?.clipboard?.writeText
54+
const hasLegacySupport = typeof document !== 'undefined' && typeof document.queryCommandSupported === 'function'
55+
&& document.queryCommandSupported('copy')
56+
57+
isSupported.value = hasClipboardAPI || hasLegacySupport
58+
}
59+
60+
const resetCopiedFlag = () => {
61+
if (resetTimer)
62+
clearTimeout(resetTimer)
63+
resetTimer = setTimeout(() => {
64+
copied.value = false
65+
resetTimer = undefined
66+
}, 1500)
67+
}
68+
69+
const copy = async (text: string) => {
70+
if (typeof window === 'undefined')
71+
throw new Error('Clipboard not available during SSR')
72+
73+
const nav = window.navigator
74+
75+
if (nav?.clipboard?.writeText) {
76+
await nav.clipboard.writeText(text)
77+
}
78+
else if (typeof document !== 'undefined' && document.body) {
79+
const textarea = document.createElement('textarea')
80+
textarea.value = text
81+
textarea.setAttribute('readonly', 'true')
82+
textarea.style.position = 'absolute'
83+
textarea.style.left = '-9999px'
84+
document.body.appendChild(textarea)
85+
textarea.select()
86+
const successful = document.execCommand?.('copy') ?? false
87+
document.body.removeChild(textarea)
88+
if (!successful)
89+
throw new Error('Clipboard copy failed')
90+
}
91+
else {
92+
throw new Error('Clipboard API not supported')
93+
}
94+
95+
copied.value = true
96+
resetCopiedFlag()
97+
}
98+
99+
if (typeof window !== 'undefined')
100+
determineSupport()
101+
102+
onBeforeUnmount(() => {
103+
if (resetTimer)
104+
clearTimeout(resetTimer)
105+
})
106+
107+
const generatedMarkdownPath = computed(() => {
108+
const filePath = page.value.relativePath || page.value.filePath || 'index.md'
109+
110+
if (filePath === 'index.md')
111+
return '/index.md'
112+
113+
if (filePath.endsWith('/index.md')) {
114+
const trimmed = filePath.slice(0, -'/index.md'.length)
115+
return `/${trimmed}.md`
116+
}
117+
118+
return `/${filePath}`
119+
})
120+
121+
const markdownUrl = computed(() => {
122+
const base = site.value.base?.replace(/\/$/, '') ?? ''
123+
const path = generatedMarkdownPath.value
124+
return `${base}${path}`
125+
})
126+
127+
const getAbsoluteMarkdownUrl = () => {
128+
if (typeof window === 'undefined')
129+
return ''
130+
131+
const origin = window.location.origin
132+
return new URL(markdownUrl.value, origin).href
133+
}
19134

20135
const showSourceCode = computed(() => {
21136
if (!isSupported.value)
@@ -112,11 +227,8 @@ export function useSourceCode() {
112227
return
113228
}
114229

115-
const currentPath = window.location.pathname
116-
const markdownPath = currentPath.replace(/\.html$/, '.md').replace(/\/$/, '/index.md')
117-
const markdownUrl = `${window.location.origin}${markdownPath}`
230+
const response = await fetch(markdownUrl.value)
118231

119-
const response = await fetch(markdownUrl)
120232
if (!response.ok) {
121233
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
122234
}
@@ -139,10 +251,7 @@ export function useSourceCode() {
139251
}
140252

141253
const pageTitle = page.value.title || document.title || 'Documentation Page'
142-
const currentPath = window.location.pathname
143-
const markdownPath = currentPath.replace(/\.html$/, '.md').replace(/\/$/, '/index.md')
144-
const markdownUrl = `${window.location.origin}${markdownPath}`
145-
const markdownLink = `[${pageTitle}](${markdownUrl})`
254+
const markdownLink = `[${pageTitle}](${getAbsoluteMarkdownUrl()})`
146255

147256
await copy(markdownLink)
148257
toast.success('Markdown link copied to clipboard')
@@ -154,34 +263,37 @@ export function useSourceCode() {
154263
}
155264

156265
const getRawMarkdownUrl = () => {
157-
let rawUrl = sourceCodeUrl.value
266+
const absoluteUrl = getAbsoluteMarkdownUrl()
158267

159-
if (rawUrl.includes('github.com') && rawUrl.includes('/blob/')) {
160-
rawUrl = rawUrl
161-
.replace('github.com', 'raw.githubusercontent.com')
162-
.replace('/blob/', '/')
163-
}
268+
if (!absoluteUrl)
269+
return ''
164270

165-
return encodeURIComponent(rawUrl)
271+
return encodeURIComponent(absoluteUrl)
166272
}
167273

168274
const chatGPTUrl = computed(() => {
169275
const rawUrl = getRawMarkdownUrl()
276+
if (!rawUrl)
277+
return ''
278+
170279
const message = `Read ${rawUrl} so I can ask questions about it.`
171280
const encodedMessage = encodeURIComponent(message)
172281
return `https://chatgpt.com/?hints=search&q=${encodedMessage}`
173282
})
174283

175284
const claudeUrl = computed(() => {
176285
const rawUrl = getRawMarkdownUrl()
286+
if (!rawUrl)
287+
return ''
288+
177289
const message = `Read ${rawUrl} so I can ask questions about it.`
178290
const encodedMessage = encodeURIComponent(message)
179291
return `https://claude.ai/new?q=${encodedMessage}`
180292
})
181293

182294
const viewAsMarkdown = () => {
183295
if (typeof window !== 'undefined') {
184-
window.open(sourceCodeUrl.value, '_blank', 'noopener,noreferrer')
296+
window.open(getAbsoluteMarkdownUrl() || markdownUrl.value, '_blank', 'noopener,noreferrer')
185297
}
186298
}
187299

0 commit comments

Comments
 (0)