@iamdangavin/claude-skill-vitepress-docs 1.5.0 → 1.8.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.
- package/package.json +1 -1
- package/skill/SKILL.md +228 -5
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -414,6 +414,34 @@ If "Somewhere else": ask as plain text — "What path should I use? (Full path o
|
|
|
414
414
|
|
|
415
415
|
Always show the full resolved path — never use abstract terms like "project root."
|
|
416
416
|
|
|
417
|
+
**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:
|
|
418
|
+
|
|
419
|
+
- header: "API base URL"
|
|
420
|
+
- 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."
|
|
421
|
+
- options:
|
|
422
|
+
- "Same as the local dev URL already provided"
|
|
423
|
+
- "Different URL — I'll type it"
|
|
424
|
+
- "Skip — I don't want code samples"
|
|
425
|
+
|
|
426
|
+
If different: ask as plain text — "What is the API base URL? (e.g. `http://localhost:8888/wp-json` or `https://api.mysite.com`)"
|
|
427
|
+
|
|
428
|
+
Then ask:
|
|
429
|
+
|
|
430
|
+
- header: "Code sample tabs"
|
|
431
|
+
- question: "Which code sample types should the API endpoint component show?"
|
|
432
|
+
- options:
|
|
433
|
+
- "Fetch + cURL (default)"
|
|
434
|
+
- "Fetch only"
|
|
435
|
+
- "cURL only"
|
|
436
|
+
- "Fetch + cURL + PHP"
|
|
437
|
+
- "Custom — I'll tell you which"
|
|
438
|
+
|
|
439
|
+
If custom: ask as plain text — "Which languages or methods? (e.g. Fetch, cURL, PHP, Python, Axios)"
|
|
440
|
+
|
|
441
|
+
Store the selected tabs — they determine which tab options are rendered in the `ApiEndpoint` component and which code samples are generated per endpoint.
|
|
442
|
+
|
|
443
|
+
Store `apiBase` and `tabs` — both used in VitePress config and the `ApiEndpoint` component.
|
|
444
|
+
|
|
417
445
|
**Q5 — Skip anything:**
|
|
418
446
|
- header: "Exclusions"
|
|
419
447
|
- question: "Anything to exclude from analysis? (node_modules, dist, .git, .next, coverage are always skipped.)"
|
|
@@ -483,6 +511,17 @@ Write pages one at a time. For each page:
|
|
|
483
511
|
```
|
|
484
512
|
Then generate the placeholder PNG (see **Placeholder generation** below).
|
|
485
513
|
|
|
514
|
+
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:
|
|
515
|
+
```md
|
|
516
|
+
<ApiEndpoint
|
|
517
|
+
method="GET"
|
|
518
|
+
path="/wp-json/alabama-news/v1/sources"
|
|
519
|
+
auth="none"
|
|
520
|
+
:response="{ sources: ['AP', 'Reuters'] }"
|
|
521
|
+
/>
|
|
522
|
+
```
|
|
523
|
+
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.
|
|
524
|
+
|
|
486
525
|
Do not stop to ask gap questions mid-page. Keep writing and accumulate all gaps.
|
|
487
526
|
|
|
488
527
|
### Step 5 — Gap review
|
|
@@ -500,10 +539,187 @@ the correct information. Can you fill these in?
|
|
|
500
539
|
|
|
501
540
|
Go back and fill in the answers the user provides.
|
|
502
541
|
|
|
503
|
-
### Step 6 —
|
|
542
|
+
### Step 6 — Install VitePress components
|
|
543
|
+
|
|
544
|
+
If `apiBase` was collected in Q1b, write the following files before updating the config.
|
|
545
|
+
|
|
546
|
+
**`.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:
|
|
547
|
+
|
|
548
|
+
```vue
|
|
549
|
+
<template>
|
|
550
|
+
<div class="api-ep">
|
|
551
|
+
<div class="api-ep__header">
|
|
552
|
+
<div class="api-ep__badge">
|
|
553
|
+
<span class="api-ep__method" :class="`api-ep__method--${method.toLowerCase()}`">{{ method }}</span>
|
|
554
|
+
<code class="api-ep__path">{{ path }}</code>
|
|
555
|
+
</div>
|
|
556
|
+
<span class="api-ep__auth">{{ authLabel }}</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="api-ep__body">
|
|
559
|
+
<div class="api-ep__tabs">
|
|
560
|
+
<button
|
|
561
|
+
v-for="tab in tabs"
|
|
562
|
+
:key="tab"
|
|
563
|
+
class="api-ep__tab"
|
|
564
|
+
:class="{ 'api-ep__tab--active': activeTab === tab }"
|
|
565
|
+
@click="activeTab = tab"
|
|
566
|
+
>{{ tab }}</button>
|
|
567
|
+
</div>
|
|
568
|
+
<pre class="api-ep__code"><code>{{ currentSample }}</code></pre>
|
|
569
|
+
<p v-if="activeTab === 'Fetch'" class="api-ep__note">
|
|
570
|
+
Must be called from an authenticated browser session — cookies are sent automatically.
|
|
571
|
+
</p>
|
|
572
|
+
<template v-if="response">
|
|
573
|
+
<div class="api-ep__section-label">Sample Response</div>
|
|
574
|
+
<pre class="api-ep__code"><code>{{ formattedResponse }}</code></pre>
|
|
575
|
+
</template>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</template>
|
|
579
|
+
|
|
580
|
+
<script setup>
|
|
581
|
+
import { ref, computed } from 'vue'
|
|
582
|
+
import { useData } from 'vitepress'
|
|
583
|
+
|
|
584
|
+
const props = defineProps({
|
|
585
|
+
method: { type: String, required: true },
|
|
586
|
+
path: { type: String, required: true },
|
|
587
|
+
auth: { type: String, default: 'authenticated' },
|
|
588
|
+
body: { type: Object, default: null },
|
|
589
|
+
response: { type: Object, default: null },
|
|
590
|
+
formData: { type: Boolean, default: false },
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
const { theme } = useData()
|
|
594
|
+
const baseUrl = computed(() => theme.value.apiBase ?? '')
|
|
595
|
+
|
|
596
|
+
const authLabel = computed(() => ({
|
|
597
|
+
'authenticated': 'Auth required',
|
|
598
|
+
'admin:manage': 'Admin only',
|
|
599
|
+
'self': 'Authenticated (self)',
|
|
600
|
+
'none': 'Unauthenticated',
|
|
601
|
+
}[props.auth] ?? props.auth))
|
|
602
|
+
|
|
603
|
+
const tabs = TABS // substituted from Q1b answer
|
|
604
|
+
const activeTab = ref(tabs[0])
|
|
605
|
+
|
|
606
|
+
const fetchSample = computed(() => {
|
|
607
|
+
const url = `${baseUrl.value}${props.path}`
|
|
608
|
+
if (props.method === 'GET') {
|
|
609
|
+
return `const res = await fetch('${url}', {\n credentials: 'include'\n})\nconst data = await res.json()`
|
|
610
|
+
}
|
|
611
|
+
if (props.formData) {
|
|
612
|
+
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()`
|
|
613
|
+
}
|
|
614
|
+
const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
|
|
615
|
+
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()`
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
const curlSample = computed(() => {
|
|
619
|
+
const url = `${baseUrl.value}${props.path}`
|
|
620
|
+
if (props.method === 'GET') {
|
|
621
|
+
return `curl '${url}' \\\n -H 'Cookie: <session-cookie>'`
|
|
622
|
+
}
|
|
623
|
+
if (props.formData) {
|
|
624
|
+
return `curl -X POST '${url}' \\\n -H 'Cookie: <session-cookie>' \\\n -F 'field=value'`
|
|
625
|
+
}
|
|
626
|
+
const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
|
|
627
|
+
return `curl -X ${props.method} '${url}' \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: <session-cookie>' \\\n -d '${bodyStr}'`
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
// Add additional sample computed properties here for PHP, Python, Axios etc if selected in Q1b
|
|
631
|
+
|
|
632
|
+
const currentSample = computed(() => {
|
|
633
|
+
if (activeTab.value === 'Fetch') return fetchSample.value
|
|
634
|
+
if (activeTab.value === 'cURL') return curlSample.value
|
|
635
|
+
return ''
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const formattedResponse = computed(() =>
|
|
639
|
+
props.response ? JSON.stringify(props.response, null, 2) : ''
|
|
640
|
+
)
|
|
641
|
+
</script>
|
|
642
|
+
|
|
643
|
+
<style scoped>
|
|
644
|
+
.api-ep { border: 1px solid var(--vp-c-divider); border-radius: 8px; margin: 1.5rem 0 2.5rem; overflow: hidden; }
|
|
645
|
+
.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); }
|
|
646
|
+
.api-ep__badge { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
647
|
+
.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; }
|
|
648
|
+
.api-ep__method--get { background: var(--vp-c-green-soft); color: var(--vp-c-green-1); }
|
|
649
|
+
.api-ep__method--post { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
|
|
650
|
+
.api-ep__method--put { background: var(--vp-c-yellow-soft); color: var(--vp-c-yellow-1); }
|
|
651
|
+
.api-ep__method--delete { background: var(--vp-c-danger-soft); color: var(--vp-c-danger-1); }
|
|
652
|
+
.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; }
|
|
653
|
+
.api-ep__auth { font-size: 0.7rem; font-weight: 500; color: var(--vp-c-text-3); flex-shrink: 0; white-space: nowrap; }
|
|
654
|
+
.api-ep__body { padding: 16px; background: var(--vp-c-bg-soft); display: flex; flex-direction: column; gap: 8px; }
|
|
655
|
+
.api-ep__tabs { display: flex; gap: 4px; margin-bottom: 2px; }
|
|
656
|
+
.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; }
|
|
657
|
+
.api-ep__tab:hover { color: var(--vp-c-text-1); background: var(--vp-c-default-soft); }
|
|
658
|
+
.api-ep__tab--active { border-color: var(--vp-c-divider); background: var(--vp-c-bg); color: var(--vp-c-text-1); }
|
|
659
|
+
.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); }
|
|
660
|
+
.api-ep__code code { background: none; padding: 0; font-size: inherit; color: inherit; }
|
|
661
|
+
.api-ep__note { font-size: 0.7rem; color: var(--vp-c-text-3); margin: 0; line-height: 1.5; }
|
|
662
|
+
.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; }
|
|
663
|
+
</style>
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Install `medium-zoom`** in the docs folder:
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
cd DOCS_FOLDER && npm install medium-zoom
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**`.vitepress/theme/index.css`** — create if it doesn't exist, otherwise append:
|
|
673
|
+
|
|
674
|
+
```css
|
|
675
|
+
.medium-zoom-overlay {
|
|
676
|
+
z-index: 20;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.medium-zoom-image {
|
|
680
|
+
z-index: 21;
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**`.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:
|
|
685
|
+
|
|
686
|
+
```js
|
|
687
|
+
import DefaultTheme from 'vitepress/theme'
|
|
688
|
+
import { onMounted, watch, nextTick } from 'vue'
|
|
689
|
+
import { useRoute } from 'vitepress'
|
|
690
|
+
import mediumZoom from 'medium-zoom'
|
|
691
|
+
import './index.css'
|
|
692
|
+
import ApiEndpoint from '../components/ApiEndpoint.vue'
|
|
693
|
+
|
|
694
|
+
export default {
|
|
695
|
+
extends: DefaultTheme,
|
|
696
|
+
setup() {
|
|
697
|
+
const route = useRoute()
|
|
698
|
+
const initZoom = () => mediumZoom('.main img', { background: 'var(--vp-c-bg)' })
|
|
699
|
+
onMounted(initZoom)
|
|
700
|
+
watch(() => route.path, () => nextTick(initZoom))
|
|
701
|
+
},
|
|
702
|
+
enhanceApp({ app }) {
|
|
703
|
+
app.component('ApiEndpoint', ApiEndpoint)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
If the file already exists, merge only the missing pieces — do not overwrite other registrations or setup logic.
|
|
709
|
+
|
|
710
|
+
### Step 6b — Update VitePress config sidebar
|
|
504
711
|
|
|
505
712
|
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.
|
|
506
713
|
|
|
714
|
+
If `apiBase` was collected, also add it to `themeConfig`:
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
themeConfig: {
|
|
718
|
+
apiBase: 'API_BASE_URL',
|
|
719
|
+
// ...existing themeConfig
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
507
723
|
### Step 7 — Write the manifest
|
|
508
724
|
|
|
509
725
|
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).
|
|
@@ -655,22 +871,29 @@ Use Playwright via Homebrew for all captures:
|
|
|
655
871
|
import { chromium } from '/opt/homebrew/lib/node_modules/playwright/index.mjs';
|
|
656
872
|
```
|
|
657
873
|
|
|
658
|
-
**⛔ Credential rule — NEVER write credentials to any file.** Do not write them to
|
|
874
|
+
**⛔ Credential rule — NEVER write credentials to any file.** Do not write them to a script file, `.env`, the manifest, or anywhere on disk. All Playwright scripts run as inline bash heredocs — credentials are passed directly as inline values to `page.fill()` calls within the heredoc and exist only in memory for the duration of the command. If Playwright needs credentials in a reusable way, use a saved storage state file — but prompt the user for credentials fresh each time rather than caching them.
|
|
659
875
|
|
|
660
876
|
**For pages requiring auth:** if credentials were provided, drive a login flow before navigating to the target URL. If no credentials were given, skip auth-gated pages and list them in the final summary so the user can run screenshot mode again with credentials.
|
|
661
877
|
|
|
662
878
|
**Standard capture script template:**
|
|
663
|
-
|
|
879
|
+
|
|
880
|
+
Run inline via bash heredoc — no file is ever written to disk:
|
|
881
|
+
|
|
882
|
+
```bash
|
|
883
|
+
node --input-type=module << 'SCRIPT'
|
|
664
884
|
import { chromium } from '/opt/homebrew/lib/node_modules/playwright/index.mjs';
|
|
665
885
|
const browser = await chromium.launch({ channel: 'chrome', args: ['--no-sandbox'] });
|
|
666
886
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
667
887
|
await page.goto('BASE_URL + captureUrl', { waitUntil: 'load', timeout: 60000 });
|
|
668
|
-
await page.waitForTimeout(
|
|
888
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
669
889
|
await page.screenshot({ path: 'OUTPUT_PATH', fullPage: false });
|
|
670
890
|
await browser.close();
|
|
891
|
+
SCRIPT
|
|
671
892
|
```
|
|
672
893
|
|
|
673
|
-
Adjust
|
|
894
|
+
Adjust `SETTLE_MS` based on stack type: WordPress/non-SPA → `500`, Next.js/SPA → `2000`–`4000`.
|
|
895
|
+
|
|
896
|
+
Never write the script to a file — always use the heredoc form above. This applies to auth flows too: inline all credential usage directly in the heredoc, never in a file.
|
|
674
897
|
|
|
675
898
|
After each capture, display the image inline with the Read tool so the user can verify it before moving on.
|
|
676
899
|
|