@iamdangavin/claude-skill-vitepress-docs 3.0.0 → 3.0.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.
@@ -0,0 +1,626 @@
1
+ ---
2
+ name: vitedocs:generate
3
+ description: Analyze the codebase and write VitePress documentation pages.
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Edit
8
+ - Bash
9
+ - Glob
10
+ - Grep
11
+ - AskUserQuestion
12
+ ---
13
+
14
+ ## Manifest
15
+
16
+ All modes read and write a manifest at `.vitepress/docs-manifest.json`. This file is **local-only** — always add it to `.gitignore` (under the docs folder's nearest `.gitignore`) so it is never committed or distributed.
17
+
18
+ Add this line if not already present:
19
+ ```
20
+ .vitepress/docs-manifest.json
21
+ ```
22
+
23
+ ### Manifest schema
24
+
25
+ ```json
26
+ {
27
+ "generated": "ISO-8601 timestamp",
28
+ "project": {
29
+ "type": "user-facing | developer | both",
30
+ "baseUrl": "http://localhost:3000",
31
+ "serverType": "node | wordpress | static | other",
32
+ "startCommand": "npm run dev"
33
+ },
34
+ "pages": [
35
+ {
36
+ "file": "docs/guides/timer.md",
37
+ "title": "Running a Timer",
38
+ "docType": "user-facing",
39
+ "sources": ["src/state/timer.js", "src/components/layouts/workouts.jsx"],
40
+ "images": [
41
+ {
42
+ "path": "docs/public/screenshots/timer-main.png",
43
+ "placeholder": true,
44
+ "caption": "Timer in active interval state",
45
+ "captureUrl": "/workouts/123",
46
+ "captureNote": "Timer should be running with intervals visible"
47
+ }
48
+ ],
49
+ "lastSynced": "ISO-8601 timestamp",
50
+ "syncHash": "sha of source file contents at last sync"
51
+ }
52
+ ]
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Mode: generate
59
+
60
+ ### Step 0 — Detect tech stack
61
+
62
+ Before asking any questions, scan the codebase root for stack markers. Use Glob and Grep — do not ask the user what their stack is until you've made an attempt to detect it yourself.
63
+
64
+ **WordPress indicators** — check for any of these:
65
+ ```
66
+ wp-config.php
67
+ wp-content/
68
+ wp-includes/
69
+ functions.php (in a theme directory)
70
+ style.css with "Theme Name:" header
71
+ composer.json with "johnpbloch/wordpress" or "roots/wordpress"
72
+ ```
73
+
74
+ **Node/Next indicators:**
75
+ ```
76
+ next.config.js / next.config.ts / next.config.mjs
77
+ package.json with "next" dependency
78
+ .next/ directory
79
+ ```
80
+
81
+ **Other Node indicators:** `package.json`, `vite.config.*`, `astro.config.*`, `nuxt.config.*`
82
+
83
+ **Static:** no package.json, no wp-config.php, only HTML/CSS/JS files.
84
+
85
+ After scanning, present your finding and confirm with the user:
86
+
87
+ - header: "Tech stack detected"
88
+ - question: "I detected this as a [DETECTED_STACK] project — is that right?"
89
+ - options:
90
+ - "Yes, that's correct"
91
+ - "No — let me tell you what it actually is"
92
+
93
+ If the user corrects you, accept their answer and proceed with the corrected stack.
94
+
95
+ ---
96
+
97
+ **If WordPress is detected**, ask the following WP-specific questions before running the git root check:
98
+
99
+ **WP-Q1 — WordPress project type:**
100
+ - header: "WordPress setup"
101
+ - question: "What kind of WordPress project is this?"
102
+ - options:
103
+ - "Traditional theme (PHP templates, The Loop, etc.)"
104
+ - "Block theme (Full Site Editing / Gutenberg blocks)"
105
+ - "Headless WordPress (REST API or WPGraphQL feeding a front-end)"
106
+ - "Plugin (documenting a plugin, not a theme)"
107
+ - "Full site — theme + plugins together"
108
+
109
+ **WP-Q2 — Local dev environment** (plain text): Ask — "What URL is the WordPress site running on locally? (e.g. `http://localhost:8888`, `http://mysite.local`)"
110
+
111
+ **WP-Q3 — Page builder / block editor:**
112
+ - header: "Page builder"
113
+ - question: "Is this site built with a page builder or block toolkit?"
114
+ - options:
115
+ - "Standard Gutenberg / block editor only"
116
+ - "Elementor"
117
+ - "Divi"
118
+ - "ACF Blocks (Advanced Custom Fields)"
119
+ - "Other — I'll tell you"
120
+ - "No page builder — mostly PHP templates"
121
+
122
+ **WP-Q4 — Key plugins to document** (plain text): Ask — "Are there any plugins that are central to how the site works and should be included in the docs? (e.g. WooCommerce, ACF, Yoast — or 'none')"
123
+
124
+ **WP-Q5 — Headless front-end** (only if headless was selected in WP-Q1):
125
+ - header: "Front-end"
126
+ - question: "What is the headless front-end stack?"
127
+ - options:
128
+ - "Next.js"
129
+ - "Nuxt"
130
+ - "SvelteKit"
131
+ - "Astro"
132
+ - "Other — I'll tell you"
133
+
134
+ **WP-Q6 — Theme / plugin folder(s)** (plain text): Scan `wp-content/themes/` and `wp-content/plugins/` for likely candidates first — present your best guess and let the user confirm or correct it. Ask — "Which theme or plugin should I focus on? You can provide multiple paths if they work together. (e.g. `wp-content/themes/my-theme, wp-content/plugins/my-plugin`)"
135
+
136
+ Once WP-Q6 is answered, the confirmed paths become the **focus paths**. All subsequent analysis, docs placement, and git root detection are scoped to these paths — the WP install root is off-limits.
137
+
138
+ **Each focus path must be its own git repository.** Git root detection (below) will validate this for every path provided. If any path is missing a `.git`, halt and ask the user to resolve it before continuing — do not proceed with a partial set.
139
+
140
+ Store all WP answers and carry them through the rest of generate mode. They affect codebase analysis (Step 2) and the doc structure proposal (Step 3).
141
+
142
+ ---
143
+
144
+ #### Git root detection (all stacks)
145
+
146
+ **For WordPress:** run this check against every path confirmed in WP-Q6.
147
+ **For all other stacks:** run this check against the detected codebase root (current working directory unless corrected above).
148
+
149
+ For each focus path, check for a `.git` directory at **that exact path**:
150
+
151
+ ```bash
152
+ ls FOCUS_PATH/.git
153
+ ```
154
+
155
+ Do not check parent folders — the `.git` must be in the specific folder being documented. For WordPress this means checking inside `wp-content/themes/my-theme/` or `wp-content/plugins/my-plugin/`, not in `wp-content/themes/`, `wp-content/`, or the WP install root.
156
+
157
+ **If `.git` is found:** no question needed — the repo root and focus path are the same. Proceed.
158
+
159
+ **If `.git` is not found:** halt and tell the user:
160
+ > "I didn't find a `.git` folder at [FOCUS_PATH]. Each documented path must be its own git repository. Please initialise a repo there and come back, or remove this path from the list."
161
+
162
+ Do not continue until every focus path has a confirmed `.git`. Each path gets its own independent docs output, manifest, and GitHub Actions workflow.
163
+
164
+ This matters because the GitHub Actions workflow file must go in `.github/workflows/` at the **git root** of each respective path.
165
+
166
+ ---
167
+
168
+ ### Step 1 — Gather project details
169
+
170
+ **Q1 — Doc type:**
171
+ - header: "Documentation type"
172
+ - question: "What type of docs should I generate?"
173
+ - options:
174
+ - "User-facing guides — how to use the app"
175
+ - "Developer reference — architecture, API, data layer"
176
+ - "Both"
177
+
178
+ **Q2 — App description** (plain text): Ask — "What is this app called, and what does it do in one sentence? (Used for headings and introductions.)"
179
+
180
+ **Q3 — Codebase root** (plain text): Ask — "Where is the codebase root? (Press enter to use the current directory.)" — **Skip for WordPress; already established by WP-Q6.**
181
+
182
+ **Q4 — Docs output:** Before asking, check whether `setup` has already been run by looking for `.vitepress/config.mjs` anywhere inside the focus path:
183
+
184
+ ```bash
185
+ find FOCUS_PATH -name "config.mjs" -path "*/.vitepress/*" 2>/dev/null
186
+ ```
187
+
188
+ **If found:** docs location is already established — skip this question, infer the docs folder from the found path, and tell the user: "I found an existing VitePress config at `[PATH]` — using that as the docs folder."
189
+
190
+ **If not found:** ask:
191
+
192
+ - header: "Docs location"
193
+ - question: "Where should the docs folder be created? I found your repo at `[GIT_ROOT_PATH]`."
194
+ - options:
195
+ - "Here → `[GIT_ROOT_PATH]/docs/`"
196
+ - "Somewhere else — I'll type the path"
197
+
198
+ If "Somewhere else": ask as plain text — "What path should I use? (Full path or relative to `[GIT_ROOT_PATH]`)"
199
+
200
+ Always show the full resolved path — never use abstract terms like "project root."
201
+
202
+ **Q1b — API base URL** (only if developer or both selected): Scan for API endpoints first — look for REST API routes, WP REST API extensions, Next.js API routes, Express routers, etc. If any are found, ask:
203
+
204
+ - header: "API base URL"
205
+ - question: "I found API endpoints in the codebase. What is the base URL for your API? This is used to generate live code samples in the docs."
206
+ - options:
207
+ - "Same as the local dev URL already provided"
208
+ - "Different URL — I'll type it"
209
+ - "Skip — I don't want code samples"
210
+
211
+ If different: ask as plain text — "What is the API base URL? (e.g. `http://localhost:8888/wp-json` or `https://api.mysite.com`)"
212
+
213
+ Then ask:
214
+
215
+ - header: "Code sample tabs"
216
+ - question: "Which code sample types should the API endpoint component show?"
217
+ - options:
218
+ - "Fetch + cURL (default)"
219
+ - "Fetch only"
220
+ - "cURL only"
221
+ - "Fetch + cURL + PHP"
222
+ - "Custom — I'll tell you which"
223
+
224
+ If custom: ask as plain text — "Which languages or methods? (e.g. Fetch, cURL, PHP, Python, Axios)"
225
+
226
+ Store the selected tabs — they determine which tab options are rendered in the `ApiEndpoint` component and which code samples are generated per endpoint.
227
+
228
+ Store `apiBase` and `tabs` — both used in VitePress config and the `ApiEndpoint` component.
229
+
230
+ **Q5 — Skip anything:**
231
+ - header: "Exclusions"
232
+ - question: "Anything to exclude from analysis? (node_modules, dist, .git, .next, coverage are always skipped.)"
233
+ - options:
234
+ - "No, defaults are fine"
235
+ - "Yes — I'll tell you what to skip"
236
+
237
+ If yes: ask as plain text — "Which folders or files should I skip?"
238
+
239
+ Wait for all answers.
240
+
241
+ ### Step 2 — Analyze the codebase
242
+
243
+ #### Scope rule
244
+
245
+ **Single focus path:** analysis is strictly confined to that directory. Do not read, Glob, or Grep anywhere outside it — including the WP install root, other themes, other plugins, or WP core.
246
+
247
+ **Multiple focus paths:** analysis is confined to the declared paths combined. Cross-reading between them is allowed and expected — if the theme references a function from the plugin, follow that reference. Nothing outside the declared set is fair game.
248
+
249
+ In both cases: if a file inside a focus path imports or references something outside the declared paths, read only that specific referenced file — do not expand analysis to its parent directory.
250
+
251
+ Scan the codebase systematically. Build a mental map before writing anything. Look for:
252
+
253
+ **For user-facing docs:**
254
+ - App routes / pages (what screens exist)
255
+ - Key user flows (what can a user do)
256
+ - Forms and interactive features
257
+ - Settings and configuration surfaces
258
+
259
+ **For developer docs:**
260
+ - Entry points and routing structure
261
+ - Data layer (API routes, DB queries, service modules)
262
+ - State management
263
+ - Auth and middleware
264
+ - Reusable utilities and lib functions
265
+ - Environment variable requirements
266
+
267
+ Use Glob and Grep to find these. Do not read every file — read enough to understand what each area does.
268
+
269
+ ### Step 3 — Plan the doc structure
270
+
271
+ Before writing any pages, output a proposed outline:
272
+
273
+ Present the proposed outline as plain text, then use AskUserQuestion to confirm:
274
+
275
+ - header: "Doc structure"
276
+ - question: "Does this outline look right?"
277
+ - options:
278
+ - "Looks good — start writing"
279
+ - "I want to make changes"
280
+
281
+ If changes: ask as plain text — "What would you like to add, remove, or rename?" Then re-present the updated outline and ask again.
282
+
283
+ ### Step 4 — Write pages
284
+
285
+ Write pages one at a time. For each page:
286
+
287
+ 1. Read the relevant source files
288
+ 2. Write the markdown based on what you can confidently determine
289
+ 3. Where information is unclear or missing, insert a gap comment and continue:
290
+ ```md
291
+ <!-- GAP: What are the valid values for rotation frequency? -->
292
+ ```
293
+ 4. Where a screenshot would help understanding, insert a placeholder image and record it in the manifest:
294
+ ```md
295
+ ![Timer in active interval state](../public/screenshots/timer-main.png)
296
+ ```
297
+ Then generate the placeholder PNG (see **Placeholder generation** below).
298
+
299
+ 5. For any page documenting API endpoints (REST routes, WP REST API extensions, etc.), use the `ApiEndpoint` component instead of plain code blocks — but only if `apiBase` was collected in Q1b. Use it like this:
300
+ ```md
301
+ <ApiEndpoint
302
+ method="GET"
303
+ path="/wp-json/alabama-news/v1/sources"
304
+ auth="none"
305
+ :response="{ sources: ['AP', 'Reuters'] }"
306
+ />
307
+ ```
308
+ Populate `method`, `path`, `auth`, `body`, and `response` from what you read in the source files. If a request body or response shape is unclear, leave it as a gap comment.
309
+
310
+ Do not stop to ask gap questions mid-page. Keep writing and accumulate all gaps.
311
+
312
+ ### Step 5 — Gap review
313
+
314
+ After all pages are written, present all gaps in one batch:
315
+
316
+ ```
317
+ I've written all X pages. I hit N gaps where I couldn't confidently determine
318
+ the correct information. Can you fill these in?
319
+
320
+ 1. [guides/timer.md] What are the valid values for rotation frequency?
321
+ 2. [developer/auth.md] Is the OAuth callback scoped to a single provider or does it support multiple?
322
+ ...
323
+ ```
324
+
325
+ Go back and fill in the answers the user provides.
326
+
327
+ ### Step 6 — Install VitePress components
328
+
329
+ If `apiBase` was collected in Q1b, write the following files before updating the config.
330
+
331
+ **`.vitepress/components/ApiEndpoint.vue`** — write this file exactly, substituting `TABS` with the selected tab labels (e.g. `['Fetch', 'cURL']`) and adding sample generators for any additional tabs beyond Fetch and cURL:
332
+
333
+ ```vue
334
+ <template>
335
+ <div class="api-ep">
336
+ <div class="api-ep__header">
337
+ <div class="api-ep__badge">
338
+ <span class="api-ep__method" :class="`api-ep__method--${method.toLowerCase()}`">{{ method }}</span>
339
+ <code class="api-ep__path">{{ path }}</code>
340
+ </div>
341
+ <span class="api-ep__auth">{{ authLabel }}</span>
342
+ </div>
343
+ <div class="api-ep__body">
344
+ <div class="api-ep__tabs">
345
+ <button
346
+ v-for="tab in tabs"
347
+ :key="tab"
348
+ class="api-ep__tab"
349
+ :class="{ 'api-ep__tab--active': activeTab === tab }"
350
+ @click="activeTab = tab"
351
+ >{{ tab }}</button>
352
+ </div>
353
+ <pre class="api-ep__code"><code v-html="highlightedSample"></code></pre>
354
+ <p v-if="activeTab === 'Fetch'" class="api-ep__note">
355
+ Must be called from an authenticated browser session — cookies are sent automatically.
356
+ </p>
357
+ <template v-if="response">
358
+ <div class="api-ep__section-label">Sample Response</div>
359
+ <pre class="api-ep__code"><code v-html="highlightedResponse"></code></pre>
360
+ </template>
361
+ </div>
362
+ </div>
363
+ </template>
364
+
365
+ <script setup>
366
+ import { ref, computed } from 'vue'
367
+ import { useData } from 'vitepress'
368
+ import hljs from 'highlight.js/lib/core'
369
+ import javascript from 'highlight.js/lib/languages/javascript'
370
+ import bash from 'highlight.js/lib/languages/bash'
371
+ import json from 'highlight.js/lib/languages/json'
372
+ // ADD THESE ONLY IF THE CORRESPONDING TAB WAS SELECTED IN Q1b:
373
+ // import php from 'highlight.js/lib/languages/php'
374
+ // import python from 'highlight.js/lib/languages/python'
375
+
376
+ hljs.registerLanguage('javascript', javascript) // Fetch + Axios
377
+ hljs.registerLanguage('bash', bash) // cURL
378
+ hljs.registerLanguage('json', json) // response bodies
379
+ // hljs.registerLanguage('php', php)
380
+ // hljs.registerLanguage('python', python)
381
+
382
+ const props = defineProps({
383
+ method: { type: String, required: true },
384
+ path: { type: String, required: true },
385
+ auth: { type: String, default: 'authenticated' },
386
+ body: { type: Object, default: null },
387
+ response: { type: Object, default: null },
388
+ formData: { type: Boolean, default: false },
389
+ })
390
+
391
+ const { theme } = useData()
392
+ const baseUrl = computed(() => theme.value.apiBase ?? '')
393
+
394
+ const authLabel = computed(() => ({
395
+ 'authenticated': 'Auth required',
396
+ 'admin:manage': 'Admin only',
397
+ 'self': 'Authenticated (self)',
398
+ 'none': 'Unauthenticated',
399
+ }[props.auth] ?? props.auth))
400
+
401
+ const tabs = TABS // substituted from Q1b answer
402
+ const activeTab = ref(tabs[0])
403
+
404
+ const fetchSample = computed(() => {
405
+ const url = `${baseUrl.value}${props.path}`
406
+ if (props.method === 'GET') {
407
+ return `const res = await fetch('${url}', {\n credentials: 'include'\n})\nconst data = await res.json()`
408
+ }
409
+ if (props.formData) {
410
+ return `const form = new FormData()\n// append fields to form...\n\nconst res = await fetch('${url}', {\n method: 'POST',\n credentials: 'include',\n body: form\n})\nconst data = await res.json()`
411
+ }
412
+ const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
413
+ return `const res = await fetch('${url}', {\n method: '${props.method}',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(${bodyStr})\n})\nconst data = await res.json()`
414
+ })
415
+
416
+ const curlSample = computed(() => {
417
+ const url = `${baseUrl.value}${props.path}`
418
+ if (props.method === 'GET') {
419
+ return `curl '${url}' \\\n -H 'Cookie: <session-cookie>'`
420
+ }
421
+ if (props.formData) {
422
+ return `curl -X POST '${url}' \\\n -H 'Cookie: <session-cookie>' \\\n -F 'field=value'`
423
+ }
424
+ const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
425
+ return `curl -X ${props.method} '${url}' \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: <session-cookie>' \\\n -d '${bodyStr}'`
426
+ })
427
+
428
+ // Add additional sample computed properties here for PHP, Python, Axios etc if selected in Q1b
429
+
430
+ const currentSample = computed(() => {
431
+ if (activeTab.value === 'Fetch') return fetchSample.value
432
+ if (activeTab.value === 'cURL') return curlSample.value
433
+ return ''
434
+ })
435
+
436
+ const tabLang = {
437
+ 'Fetch': 'javascript',
438
+ 'Axios': 'javascript',
439
+ 'cURL': 'bash',
440
+ 'PHP': 'php',
441
+ 'Python': 'python',
442
+ }
443
+
444
+ const highlight = (code, lang) => {
445
+ const registered = hljs.getLanguage(lang)
446
+ return registered
447
+ ? hljs.highlight(code, { language: lang }).value
448
+ : hljs.highlightAuto(code).value
449
+ }
450
+
451
+ const highlightedSample = computed(() =>
452
+ highlight(currentSample.value, tabLang[activeTab.value] ?? 'bash')
453
+ )
454
+
455
+ const formattedResponse = computed(() =>
456
+ props.response ? JSON.stringify(props.response, null, 2) : ''
457
+ )
458
+
459
+ const highlightedResponse = computed(() =>
460
+ formattedResponse.value ? highlight(formattedResponse.value, 'json') : ''
461
+ )
462
+ </script>
463
+
464
+ <style scoped>
465
+ .api-ep { border: 1px solid var(--vp-c-divider); border-radius: 8px; margin: 1.5rem 0 2.5rem; overflow: hidden; }
466
+ .api-ep__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; background: var(--vp-c-bg-soft); border-bottom: 1px solid var(--vp-c-divider); }
467
+ .api-ep__badge { display: flex; align-items: center; gap: 10px; min-width: 0; }
468
+ .api-ep__method { display: inline-flex; align-items: center; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; padding: 2px 7px; border-radius: 4px; flex-shrink: 0; }
469
+ .api-ep__method--get { background: var(--vp-c-green-soft); color: var(--vp-c-green-1); }
470
+ .api-ep__method--post { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
471
+ .api-ep__method--put { background: var(--vp-c-yellow-soft); color: var(--vp-c-yellow-1); }
472
+ .api-ep__method--delete { background: var(--vp-c-danger-soft); color: var(--vp-c-danger-1); }
473
+ .api-ep__path { font-family: var(--vp-font-family-mono); font-size: 0.85rem; color: var(--vp-c-text-1); background: none; padding: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
474
+ .api-ep__auth { font-size: 0.7rem; font-weight: 500; color: var(--vp-c-text-3); flex-shrink: 0; white-space: nowrap; }
475
+ .api-ep__body { padding: 16px; background: var(--vp-c-bg-soft); display: flex; flex-direction: column; gap: 8px; }
476
+ .api-ep__tabs { display: flex; gap: 4px; margin-bottom: 2px; }
477
+ .api-ep__tab { font-size: 0.75rem; font-weight: 500; padding: 3px 10px; border-radius: 4px; border: 1px solid transparent; background: transparent; color: var(--vp-c-text-2); cursor: pointer; transition: all 0.15s; }
478
+ .api-ep__tab:hover { color: var(--vp-c-text-1); background: var(--vp-c-default-soft); }
479
+ .api-ep__tab--active { border-color: var(--vp-c-divider); background: var(--vp-c-bg); color: var(--vp-c-text-1); }
480
+ .api-ep__code { margin: 0; padding: 12px; background: var(--vp-c-bg); border-radius: 6px; overflow-x: auto; font-family: var(--vp-font-family-mono); font-size: 0.72rem; line-height: 1.65; color: var(--vp-c-text-1); }
481
+ .api-ep__code code { background: none; padding: 0; font-size: inherit; color: inherit; }
482
+ .api-ep__note { font-size: 0.7rem; color: var(--vp-c-text-3); margin: 0; line-height: 1.5; }
483
+ .api-ep__section-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--vp-c-text-3); margin-top: 4px; }
484
+ </style>
485
+ ```
486
+
487
+ **Install dependencies** in the docs folder:
488
+
489
+ ```bash
490
+ cd DOCS_FOLDER && npm install medium-zoom highlight.js
491
+ ```
492
+
493
+ **`.vitepress/theme/index.css`** — create if it doesn't exist, otherwise append:
494
+
495
+ ```css
496
+ .medium-zoom-overlay {
497
+ z-index: 100;
498
+ }
499
+
500
+ .medium-zoom-image--opened {
501
+ z-index: 101;
502
+ }
503
+ ```
504
+
505
+ **`.vitepress/theme/index.js`** — create if it doesn't exist, otherwise merge into existing. This template includes both `ApiEndpoint` registration and medium-zoom. If `apiBase` was not collected, omit the `ApiEndpoint` lines:
506
+
507
+ ```js
508
+ import DefaultTheme from 'vitepress/theme'
509
+ import { onMounted, watch, nextTick } from 'vue'
510
+ import { useRoute } from 'vitepress'
511
+ import mediumZoom from 'medium-zoom'
512
+ import './index.css'
513
+ import ApiEndpoint from '../components/ApiEndpoint.vue'
514
+
515
+ export default {
516
+ extends: DefaultTheme,
517
+ setup() {
518
+ const route = useRoute()
519
+ const initZoom = () => mediumZoom('.main img', { background: 'var(--vp-c-bg)' })
520
+ onMounted(initZoom)
521
+ watch(() => route.path, () => nextTick(initZoom))
522
+ },
523
+ enhanceApp({ app }) {
524
+ app.component('ApiEndpoint', ApiEndpoint)
525
+ }
526
+ }
527
+ ```
528
+
529
+ If the file already exists, merge only the missing pieces — do not overwrite other registrations or setup logic.
530
+
531
+ ### Step 6b — Update VitePress config sidebar
532
+
533
+ Read the existing `.vitepress/config.mjs` (or `.ts`) and update the `sidebar` and `nav` to include all newly generated pages. Edit the config in place — do not rewrite unrelated sections.
534
+
535
+ If `apiBase` was collected, also add it to `themeConfig`:
536
+
537
+ ```js
538
+ themeConfig: {
539
+ apiBase: 'API_BASE_URL',
540
+ // ...existing themeConfig
541
+ }
542
+ ```
543
+
544
+ ### Step 7 — Write the manifest
545
+
546
+ Write `.vitepress/docs-manifest.json` capturing all pages, their source file mappings, image placeholder status, and a `syncHash` (SHA of source file contents at generation time — use a simple string hash if needed).
547
+
548
+ Add `docs-manifest.json` to `.vitepress/` entry in `.gitignore`.
549
+
550
+ ### Step 8 — Summary
551
+
552
+ ```
553
+ Generated X pages (Y user-facing, Z developer).
554
+ Created N placeholder screenshots — run /vitedocs:screenshot when ready to capture real images.
555
+ Filled M of P gaps — X remaining gap comments left in files for manual review.
556
+ ```
557
+
558
+ ---
559
+
560
+ ## Placeholder generation
561
+
562
+ When a page needs a screenshot, generate a 1200×630 grey PNG at the specified path before moving on.
563
+
564
+ Try in order:
565
+
566
+ **Option A — ImageMagick:**
567
+ ```bash
568
+ convert -size 1200x630 xc:'#888888' path/to/placeholder.png
569
+ ```
570
+
571
+ **Option B — Node.js (no external deps):**
572
+
573
+ Write this to `/tmp/make-placeholder.mjs`, run it, then delete it:
574
+
575
+ ```js
576
+ import zlib from 'zlib';
577
+ import { writeFileSync, mkdirSync } from 'fs';
578
+ import { dirname } from 'path';
579
+
580
+ const W = 1200, H = 630;
581
+ const OUTPUT = 'FILL_IN_OUTPUT_PATH';
582
+
583
+ const scanlines = [];
584
+ for (let y = 0; y < H; y++) {
585
+ const row = Buffer.alloc(1 + W * 3);
586
+ row[0] = 0;
587
+ row.fill(0x88, 1);
588
+ scanlines.push(row);
589
+ }
590
+ const raw = Buffer.concat(scanlines);
591
+ const compressed = zlib.deflateSync(raw, { level: 9 });
592
+
593
+ const crcTable = new Uint32Array(256);
594
+ for (let i = 0; i < 256; i++) {
595
+ let c = i;
596
+ for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
597
+ crcTable[i] = c;
598
+ }
599
+ function crc32(buf) {
600
+ let c = 0xFFFFFFFF;
601
+ for (const b of buf) c = crcTable[(c ^ b) & 0xFF] ^ (c >>> 8);
602
+ return (c ^ 0xFFFFFFFF) >>> 0;
603
+ }
604
+ function makeChunk(type, data) {
605
+ const t = Buffer.from(type, 'ascii');
606
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
607
+ const crcVal = Buffer.alloc(4); crcVal.writeUInt32BE(crc32(Buffer.concat([t, data])));
608
+ return Buffer.concat([len, t, data, crcVal]);
609
+ }
610
+
611
+ const sig = Buffer.from([137,80,78,71,13,10,26,10]);
612
+ const ihdr = Buffer.alloc(13);
613
+ ihdr.writeUInt32BE(W, 0); ihdr.writeUInt32BE(H, 4);
614
+ ihdr[8] = 8; ihdr[9] = 2;
615
+
616
+ mkdirSync(dirname(OUTPUT), { recursive: true });
617
+ writeFileSync(OUTPUT, Buffer.concat([
618
+ sig,
619
+ makeChunk('IHDR', ihdr),
620
+ makeChunk('IDAT', compressed),
621
+ makeChunk('IEND', Buffer.alloc(0))
622
+ ]));
623
+ console.log('Created', OUTPUT);
624
+ ```
625
+
626
+ If both options fail, tell the user and skip the placeholder for that image — leave the `![...](path)` reference in the markdown so it renders as a broken image, making it obvious during preview.