@claude-code-mastery/starter-kit 1.0.0 → 1.2.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/.claude/.starter-kit/profiles/clean.md +7 -1
- package/.claude/.starter-kit/profiles/go.md +1 -1
- package/.claude/.starter-kit/profiles/node.md +13 -4
- package/.claude/.starter-kit/profiles/python.md +1 -1
- package/.claude/commands/show-user-guide.md +22 -20
- package/.dockerignore +39 -0
- package/.env.example +74 -0
- package/.gitignore +70 -0
- package/CLAUDE.md +838 -0
- package/README.md +111 -2073
- package/README.npm.md +190 -0
- package/bin/cli.js +22 -0
- package/package.json +14 -4
- package/playwright.config.ts +79 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/build-content.ts +108 -0
- package/scripts/content/html-template.ts +144 -0
- package/scripts/content/markdown-processor.ts +261 -0
- package/scripts/content/seo-generator.ts +42 -0
- package/scripts/content/sidebar-generator.ts +71 -0
- package/scripts/content/types.ts +72 -0
- package/scripts/content.config.json +49 -0
- package/scripts/db-query.ts +143 -0
- package/scripts/queries/example-count-docs.ts +25 -0
- package/scripts/queries/example-find-user.ts +32 -0
- package/scripts/scaffold-clean.sh +591 -0
- package/scripts/scaffold-default.sh +1251 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder — Markdown Processor
|
|
3
|
+
*
|
|
4
|
+
* Converts markdown text to HTML with support for code blocks,
|
|
5
|
+
* tables, lists, blockquotes, headings, and inline formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CollectedHeading } from './types.js';
|
|
9
|
+
|
|
10
|
+
export function escapeHtml(text: string): string {
|
|
11
|
+
return text
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function slugify(text: string): string {
|
|
18
|
+
return text
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^\w\s-]/g, '')
|
|
21
|
+
.replace(/\s+/g, '-')
|
|
22
|
+
.replace(/-+/g, '-')
|
|
23
|
+
.replace(/^-|-$/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stripMarkdown(text: string): string {
|
|
27
|
+
return text
|
|
28
|
+
.replace(/\*\*\*(.*?)\*\*\*/g, '$1')
|
|
29
|
+
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
30
|
+
.replace(/\*(.*?)\*/g, '$1')
|
|
31
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
32
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function processInlineFormatting(text: string): string {
|
|
36
|
+
let result = text;
|
|
37
|
+
result = result.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
38
|
+
result = result.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
39
|
+
result = result.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
40
|
+
result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
41
|
+
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
42
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
43
|
+
result = result.replace(/ {2}$/gm, '<br>');
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function convertTableToHtml(rows: string[]): string {
|
|
48
|
+
if (rows.length === 0) return '';
|
|
49
|
+
let html = '<div class="table-wrapper">\n<table>\n';
|
|
50
|
+
|
|
51
|
+
rows.forEach((row, idx) => {
|
|
52
|
+
const cells = row.split('|').slice(1, -1);
|
|
53
|
+
const tag = idx === 0 ? 'th' : 'td';
|
|
54
|
+
html += '<tr>';
|
|
55
|
+
for (const cell of cells) {
|
|
56
|
+
let content = cell.trim();
|
|
57
|
+
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
58
|
+
content = content.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
59
|
+
html += `<${tag}>${content}</${tag}>`;
|
|
60
|
+
}
|
|
61
|
+
html += '</tr>\n';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
html += '</table>\n</div>';
|
|
65
|
+
return html;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function processMarkdownTables(html: string): string {
|
|
69
|
+
const lines = html.split('\n');
|
|
70
|
+
const result: string[] = [];
|
|
71
|
+
let inTable = false;
|
|
72
|
+
let tableRows: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
|
|
77
|
+
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
|
|
78
|
+
if (!inTable) {
|
|
79
|
+
inTable = true;
|
|
80
|
+
tableRows = [];
|
|
81
|
+
}
|
|
82
|
+
if (!/^\|[\s\-:|]+\|$/.test(trimmed)) {
|
|
83
|
+
tableRows.push(trimmed);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (inTable) {
|
|
87
|
+
result.push(convertTableToHtml(tableRows));
|
|
88
|
+
inTable = false;
|
|
89
|
+
tableRows = [];
|
|
90
|
+
}
|
|
91
|
+
result.push(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (inTable && tableRows.length > 0) {
|
|
96
|
+
result.push(convertTableToHtml(tableRows));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function processLists(html: string): string {
|
|
103
|
+
const lines = html.split('\n');
|
|
104
|
+
const result: string[] = [];
|
|
105
|
+
let inList = false;
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const match = line.match(/^(\s*)- (.*)$/);
|
|
109
|
+
if (match) {
|
|
110
|
+
const content = processInlineFormatting(match[2]);
|
|
111
|
+
if (!inList) {
|
|
112
|
+
result.push('<ul>');
|
|
113
|
+
inList = true;
|
|
114
|
+
}
|
|
115
|
+
result.push(`<li>${content}</li>`);
|
|
116
|
+
} else {
|
|
117
|
+
if (inList) {
|
|
118
|
+
result.push('</ul>');
|
|
119
|
+
inList = false;
|
|
120
|
+
}
|
|
121
|
+
result.push(line);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (inList) result.push('</ul>');
|
|
126
|
+
return result.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function processOrderedLists(html: string): string {
|
|
130
|
+
const lines = html.split('\n');
|
|
131
|
+
const result: string[] = [];
|
|
132
|
+
let inList = false;
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const match = line.match(/^\d+\.\s+(.*)$/);
|
|
136
|
+
if (match) {
|
|
137
|
+
if (!inList) {
|
|
138
|
+
result.push('<ol>');
|
|
139
|
+
inList = true;
|
|
140
|
+
}
|
|
141
|
+
result.push(`<li>${processInlineFormatting(match[1])}</li>`);
|
|
142
|
+
} else {
|
|
143
|
+
if (inList) {
|
|
144
|
+
result.push('</ol>');
|
|
145
|
+
inList = false;
|
|
146
|
+
}
|
|
147
|
+
result.push(line);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (inList) result.push('</ol>');
|
|
152
|
+
return result.join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function convertMarkdownToHtml(md: string, headings: CollectedHeading[]): string {
|
|
156
|
+
let html = md;
|
|
157
|
+
|
|
158
|
+
// Code blocks first (preserve content)
|
|
159
|
+
const codeBlocks: string[] = [];
|
|
160
|
+
html = html.replace(/````(\w*)\n([\s\S]*?)````/g, (_match, lang: string, code: string) => {
|
|
161
|
+
const placeholder = `___CODEBLOCK_${codeBlocks.length}___`;
|
|
162
|
+
codeBlocks.push(`<pre><code class="language-${lang || 'plaintext'}">${escapeHtml(code.trim())}</code></pre>`);
|
|
163
|
+
return placeholder;
|
|
164
|
+
});
|
|
165
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang: string, code: string) => {
|
|
166
|
+
const placeholder = `___CODEBLOCK_${codeBlocks.length}___`;
|
|
167
|
+
codeBlocks.push(`<pre><code class="language-${lang || 'plaintext'}">${escapeHtml(code.trim())}</code></pre>`);
|
|
168
|
+
return placeholder;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Tables
|
|
172
|
+
html = processMarkdownTables(html);
|
|
173
|
+
|
|
174
|
+
// Blockquotes
|
|
175
|
+
html = html.replace(/^>\s*(.*)$/gm, '<blockquote><p>$1</p></blockquote>');
|
|
176
|
+
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
177
|
+
|
|
178
|
+
// Headings (collect for sidebar TOC)
|
|
179
|
+
html = html.replace(/^#### (.*)$/gm, (_m, text: string) => {
|
|
180
|
+
const id = slugify(text);
|
|
181
|
+
headings.push({ level: 4, id, text: stripMarkdown(text) });
|
|
182
|
+
return `<h4 id="${id}">${processInlineFormatting(text)}</h4>`;
|
|
183
|
+
});
|
|
184
|
+
html = html.replace(/^### (.*)$/gm, (_m, text: string) => {
|
|
185
|
+
const id = slugify(text);
|
|
186
|
+
headings.push({ level: 3, id, text: stripMarkdown(text) });
|
|
187
|
+
return `<h3 id="${id}">${processInlineFormatting(text)}</h3>`;
|
|
188
|
+
});
|
|
189
|
+
html = html.replace(/^## (.*)$/gm, (_m, text: string) => {
|
|
190
|
+
const id = slugify(text);
|
|
191
|
+
headings.push({ level: 2, id, text: stripMarkdown(text) });
|
|
192
|
+
return `<h2 id="${id}">${processInlineFormatting(text)}</h2>`;
|
|
193
|
+
});
|
|
194
|
+
html = html.replace(/^# (.*)$/gm, (_m, text: string) => {
|
|
195
|
+
const id = slugify(text);
|
|
196
|
+
headings.push({ level: 1, id, text: stripMarkdown(text) });
|
|
197
|
+
return `<h1 id="${id}">${processInlineFormatting(text)}</h1>`;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Horizontal rules
|
|
201
|
+
html = html.replace(/^---$/gm, '<hr>');
|
|
202
|
+
|
|
203
|
+
// Lists
|
|
204
|
+
html = processLists(html);
|
|
205
|
+
html = processOrderedLists(html);
|
|
206
|
+
|
|
207
|
+
// Remaining inline formatting
|
|
208
|
+
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
209
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
210
|
+
html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
|
211
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
212
|
+
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
213
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
214
|
+
|
|
215
|
+
// Paragraphs
|
|
216
|
+
const blockTags = /^<(h[1-6]|ul|ol|li|blockquote|pre|div|table|tr|th|td|hr|nav|header|footer|article|section)/i;
|
|
217
|
+
const rawLines = html.split('\n');
|
|
218
|
+
const groups: Array<{ type: string; lines?: string[]; content?: string }> = [];
|
|
219
|
+
let currentGroup: string[] = [];
|
|
220
|
+
|
|
221
|
+
for (const line of rawLines) {
|
|
222
|
+
const trimmed = line.trim();
|
|
223
|
+
if (trimmed === '' || blockTags.test(trimmed) || trimmed.startsWith('___CODEBLOCK_')) {
|
|
224
|
+
if (currentGroup.length > 0) {
|
|
225
|
+
groups.push({ type: 'paragraph', lines: currentGroup });
|
|
226
|
+
currentGroup = [];
|
|
227
|
+
}
|
|
228
|
+
if (trimmed !== '') {
|
|
229
|
+
groups.push({ type: 'html', content: trimmed });
|
|
230
|
+
}
|
|
231
|
+
} else if (line.endsWith(' ')) {
|
|
232
|
+
currentGroup.push(trimmed + '<br>');
|
|
233
|
+
} else {
|
|
234
|
+
currentGroup.push(trimmed);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (currentGroup.length > 0) {
|
|
238
|
+
groups.push({ type: 'paragraph', lines: currentGroup });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result: string[] = [];
|
|
242
|
+
for (const group of groups) {
|
|
243
|
+
if (group.type === 'html') {
|
|
244
|
+
result.push(group.content!);
|
|
245
|
+
} else if (group.type === 'paragraph' && group.lines) {
|
|
246
|
+
result.push(`<p>${group.lines.join('\n')}</p>`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
html = result.join('\n');
|
|
251
|
+
|
|
252
|
+
// Restore code blocks
|
|
253
|
+
codeBlocks.forEach((block, i) => {
|
|
254
|
+
html = html.replace(`___CODEBLOCK_${i}___`, block);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
html = html.replace(/<p><\/p>/g, '');
|
|
258
|
+
html = html.replace(/\n{3,}/g, '\n\n');
|
|
259
|
+
|
|
260
|
+
return html;
|
|
261
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder — SEO & Schema.org Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates JSON-LD structured data for articles and FAQ pages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ArticleConfig, ContentConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
export function generateSchemaJson(article: ArticleConfig, config: ContentConfig): string {
|
|
10
|
+
const schema: Record<string, unknown> = {
|
|
11
|
+
'@context': 'https://schema.org',
|
|
12
|
+
'@type': 'Article',
|
|
13
|
+
headline: article.title,
|
|
14
|
+
description: article.description,
|
|
15
|
+
url: article.url,
|
|
16
|
+
datePublished: article.datePublished,
|
|
17
|
+
dateModified: article.datePublished,
|
|
18
|
+
author: { '@type': 'Person', name: config.author, url: config.siteUrl },
|
|
19
|
+
publisher: { '@type': 'Person', name: config.author },
|
|
20
|
+
keywords: article.keywords ?? [],
|
|
21
|
+
isPartOf: article.parent
|
|
22
|
+
? { '@type': 'Article', url: `${config.siteUrl}${article.parent.url}` }
|
|
23
|
+
: { '@type': 'WebSite', name: config.siteName, url: config.siteUrl },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let block = ` <script type="application/ld+json">\n ${JSON.stringify(schema, null, 4)}\n </script>`;
|
|
27
|
+
|
|
28
|
+
if (article.faqSchema && article.faqSchema.length > 0) {
|
|
29
|
+
const faqSchema = {
|
|
30
|
+
'@context': 'https://schema.org',
|
|
31
|
+
'@type': 'FAQPage',
|
|
32
|
+
mainEntity: article.faqSchema.map((faq) => ({
|
|
33
|
+
'@type': 'Question',
|
|
34
|
+
name: faq.question,
|
|
35
|
+
acceptedAnswer: { '@type': 'Answer', text: faq.answer },
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
block += `\n <script type="application/ld+json">\n ${JSON.stringify(faqSchema, null, 4)}\n </script>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return block;
|
|
42
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder — Sidebar Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates sidebar HTML with TOC, parent/child navigation, and grouping.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ArticleConfig, CollectedHeading } from './types.js';
|
|
8
|
+
|
|
9
|
+
export function generateSidebarHtml(article: ArticleConfig, headings: CollectedHeading[]): string {
|
|
10
|
+
if (!article.sidebar) return '';
|
|
11
|
+
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
|
|
14
|
+
if (article.parent) {
|
|
15
|
+
parts.push(` <a href="${article.parent.url}" class="sidebar-back">${article.parent.title}</a>`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const maxLevel = article.tocLevel ?? (article.parent ? 2 : 1);
|
|
19
|
+
let tocHeadings = headings.filter((h) => h.level <= maxLevel);
|
|
20
|
+
if (article.tocFilter) {
|
|
21
|
+
const re = new RegExp(article.tocFilter);
|
|
22
|
+
tocHeadings = tocHeadings.filter((h) => re.test(h.text));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (tocHeadings.length > 0) {
|
|
26
|
+
parts.push(' <div class="sidebar-label">CONTENTS</div>');
|
|
27
|
+
parts.push(' <ul class="sidebar-toc">');
|
|
28
|
+
for (const h of tocHeadings) {
|
|
29
|
+
const cls = h.level === 1 ? 'toc-h1' : 'toc-h2';
|
|
30
|
+
parts.push(` <li><a href="#${h.id}" class="${cls}">${h.text}</a></li>`);
|
|
31
|
+
}
|
|
32
|
+
parts.push(' </ul>');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (article.children && article.children.length > 0) {
|
|
36
|
+
const currentPath = '/' + article.htmlOutput.replace(/index\.html$/, '');
|
|
37
|
+
const hasGroups = article.children.some((c) => c.group);
|
|
38
|
+
|
|
39
|
+
if (hasGroups) {
|
|
40
|
+
let listOpen = false;
|
|
41
|
+
for (const child of article.children) {
|
|
42
|
+
if (child.group) {
|
|
43
|
+
if (listOpen) parts.push(' </ul>');
|
|
44
|
+
parts.push(` <div class="sidebar-label">${child.group}</div>`);
|
|
45
|
+
parts.push(' <ul class="sidebar-links">');
|
|
46
|
+
listOpen = true;
|
|
47
|
+
}
|
|
48
|
+
const isCurrent = child.url === currentPath;
|
|
49
|
+
const cls = isCurrent ? ' class="sidebar-current"' : '';
|
|
50
|
+
parts.push(` <li><a href="${child.url}"${cls}>${child.title}</a></li>`);
|
|
51
|
+
}
|
|
52
|
+
if (listOpen) parts.push(' </ul>');
|
|
53
|
+
} else {
|
|
54
|
+
const label = article.childrenLabel ?? (article.parent ? 'RELATED' : 'DEEP DIVES');
|
|
55
|
+
parts.push(` <div class="sidebar-label">${label}</div>`);
|
|
56
|
+
parts.push(' <ul class="sidebar-links">');
|
|
57
|
+
for (const child of article.children) {
|
|
58
|
+
const isCurrent = child.url === currentPath;
|
|
59
|
+
const cls = isCurrent ? ' class="sidebar-current"' : '';
|
|
60
|
+
parts.push(` <li><a href="${child.url}"${cls}>${child.title}</a></li>`);
|
|
61
|
+
}
|
|
62
|
+
parts.push(' </ul>');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return ` <aside class="article-sidebar" id="articleSidebar">
|
|
67
|
+
<div class="sidebar-inner">
|
|
68
|
+
${parts.join('\n')}
|
|
69
|
+
</div>
|
|
70
|
+
</aside>`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder — Types & Validation
|
|
3
|
+
*
|
|
4
|
+
* Shared types and Zod schemas for the content build system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Zod Schemas (runtime validation)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const SidebarChildSchema = z.object({
|
|
14
|
+
title: z.string().min(1),
|
|
15
|
+
url: z.string().min(1),
|
|
16
|
+
group: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const FaqItemSchema = z.object({
|
|
20
|
+
question: z.string().min(1),
|
|
21
|
+
answer: z.string().min(1),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const ArticleConfigSchema = z.object({
|
|
25
|
+
id: z.string().min(1, 'Article id is required'),
|
|
26
|
+
published: z.boolean(),
|
|
27
|
+
mdSource: z.string().min(1, 'mdSource path is required'),
|
|
28
|
+
htmlOutput: z.string().min(1, 'htmlOutput path is required'),
|
|
29
|
+
title: z.string().min(1, 'title is required'),
|
|
30
|
+
titleHtml: z.string().optional(),
|
|
31
|
+
subtitle: z.string().optional(),
|
|
32
|
+
description: z.string().min(1, 'description is required'),
|
|
33
|
+
bannerImage: z.string().optional(),
|
|
34
|
+
bannerAlt: z.string().optional(),
|
|
35
|
+
url: z.string().url('url must be a valid URL'),
|
|
36
|
+
datePublished: z.string().min(1, 'datePublished is required'),
|
|
37
|
+
category: z.string().optional(),
|
|
38
|
+
tags: z.array(z.string()).optional(),
|
|
39
|
+
keywords: z.array(z.string()).optional(),
|
|
40
|
+
sidebar: z.boolean().optional(),
|
|
41
|
+
parent: z.object({ title: z.string(), url: z.string() }).optional(),
|
|
42
|
+
children: z.array(SidebarChildSchema).optional(),
|
|
43
|
+
childrenLabel: z.string().optional(),
|
|
44
|
+
tocLevel: z.number().int().min(1).max(6).optional(),
|
|
45
|
+
tocFilter: z.string().optional(),
|
|
46
|
+
faqSchema: z.array(FaqItemSchema).optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ContentConfigSchema = z.object({
|
|
50
|
+
siteUrl: z.string().url('siteUrl must be a valid URL'),
|
|
51
|
+
siteName: z.string().min(1, 'siteName is required'),
|
|
52
|
+
author: z.string().min(1, 'author is required'),
|
|
53
|
+
outputDir: z.string().min(1, 'outputDir is required'),
|
|
54
|
+
categories: z.array(z.string()),
|
|
55
|
+
articles: z.array(ArticleConfigSchema),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export { ContentConfigSchema, ArticleConfigSchema };
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// TypeScript interfaces (inferred from Zod for zero drift)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export type SidebarChild = z.infer<typeof SidebarChildSchema>;
|
|
65
|
+
export type ArticleConfig = z.infer<typeof ArticleConfigSchema>;
|
|
66
|
+
export type ContentConfig = z.infer<typeof ContentConfigSchema>;
|
|
67
|
+
|
|
68
|
+
export interface CollectedHeading {
|
|
69
|
+
level: number;
|
|
70
|
+
id: string;
|
|
71
|
+
text: string;
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"siteUrl": "https://example.com",
|
|
3
|
+
"siteName": "My Project",
|
|
4
|
+
"author": "Your Name",
|
|
5
|
+
"outputDir": "public/articles",
|
|
6
|
+
"categories": ["All", "Guides", "Updates", "Tutorials"],
|
|
7
|
+
"articles": [
|
|
8
|
+
{
|
|
9
|
+
"id": "getting-started",
|
|
10
|
+
"published": true,
|
|
11
|
+
"mdSource": "content/getting-started.md",
|
|
12
|
+
"htmlOutput": "public/articles/getting-started/index.html",
|
|
13
|
+
"title": "Getting Started Guide",
|
|
14
|
+
"titleHtml": "Getting <span>Started</span>",
|
|
15
|
+
"subtitle": "Everything you need to know",
|
|
16
|
+
"description": "A comprehensive guide to getting started with the project. Covers installation, configuration, and first steps.",
|
|
17
|
+
"bannerImage": "/images/getting-started-banner.webp",
|
|
18
|
+
"bannerAlt": "Getting Started Guide",
|
|
19
|
+
"url": "https://example.com/articles/getting-started/",
|
|
20
|
+
"datePublished": "2026-01-15",
|
|
21
|
+
"category": "Guides",
|
|
22
|
+
"tags": ["Guide", "Setup", "2026"],
|
|
23
|
+
"keywords": ["getting started", "setup", "installation", "tutorial"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "example-with-sidebar",
|
|
27
|
+
"published": false,
|
|
28
|
+
"mdSource": "content/advanced-guide.md",
|
|
29
|
+
"htmlOutput": "public/articles/advanced-guide/index.html",
|
|
30
|
+
"title": "Advanced Configuration",
|
|
31
|
+
"titleHtml": "Advanced <span>Configuration</span>",
|
|
32
|
+
"subtitle": "Deep dive into settings",
|
|
33
|
+
"description": "Advanced configuration options and patterns for power users.",
|
|
34
|
+
"bannerImage": "/images/advanced-banner.webp",
|
|
35
|
+
"bannerAlt": "Advanced Configuration Guide",
|
|
36
|
+
"url": "https://example.com/articles/advanced-guide/",
|
|
37
|
+
"datePublished": "2026-02-01",
|
|
38
|
+
"category": "Tutorials",
|
|
39
|
+
"tags": ["Advanced", "Configuration", "2026"],
|
|
40
|
+
"keywords": ["advanced", "configuration", "settings", "customization"],
|
|
41
|
+
"sidebar": true,
|
|
42
|
+
"children": [
|
|
43
|
+
{ "title": "Database Setup", "url": "/articles/advanced-guide/database/" },
|
|
44
|
+
{ "title": "Authentication", "url": "/articles/advanced-guide/auth/" },
|
|
45
|
+
{ "title": "Deployment", "url": "/articles/advanced-guide/deploy/" }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* db-query.ts — Test Query Master
|
|
4
|
+
*
|
|
5
|
+
* This is the MASTER INDEX for all database test/dev queries.
|
|
6
|
+
* It is NOT production code — it exists solely for developer exploration.
|
|
7
|
+
*
|
|
8
|
+
* HOW IT WORKS:
|
|
9
|
+
* - Each query lives in its own file under scripts/queries/
|
|
10
|
+
* - Every query file is registered here with a name and description
|
|
11
|
+
* - Run: npx tsx scripts/db-query.ts <query-name> [args...]
|
|
12
|
+
* - Run: npx tsx scripts/db-query.ts --list (to see all available queries)
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS PATTERN:
|
|
15
|
+
* - Keeps test/dev database code COMPLETELY separate from production code
|
|
16
|
+
* - Every query receives a shared StrictDB instance — no raw driver imports
|
|
17
|
+
* - Easy to see at a glance what queries exist and what they do
|
|
18
|
+
* - Individual files are easy to review, modify, and delete
|
|
19
|
+
* - Production code in src/ never touches this — clean separation
|
|
20
|
+
*
|
|
21
|
+
* RULES:
|
|
22
|
+
* - EVERY query file MUST accept a StrictDB instance as its first parameter
|
|
23
|
+
* - NEVER import native database drivers directly in query files
|
|
24
|
+
* - NEVER copy query logic into production code — if you need it in prod,
|
|
25
|
+
* create a proper handler in src/handlers/
|
|
26
|
+
* - Each query file exports: { name, description, run(db, args) }
|
|
27
|
+
*
|
|
28
|
+
* Install: npm install tsx -D (if not already installed)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { StrictDB } from 'strictdb';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Query registry — add new queries here
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
interface QueryModule {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
run: (db: StrictDB, args: string[]) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register all query files here.
|
|
45
|
+
* Each entry maps a command name to its query module path.
|
|
46
|
+
*
|
|
47
|
+
* When Claude creates a new query for you, it will:
|
|
48
|
+
* 1. Create a new file in scripts/queries/<name>.ts
|
|
49
|
+
* 2. Add an entry to this registry
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
* 'user-lookup': () => import('./queries/user-lookup.js'),
|
|
53
|
+
*/
|
|
54
|
+
const queryRegistry: Record<string, () => Promise<{ default: QueryModule }>> = {
|
|
55
|
+
// --- Example queries (remove or modify as needed) ---
|
|
56
|
+
'example-find-user': () => import('./queries/example-find-user.js'),
|
|
57
|
+
'example-count-docs': () => import('./queries/example-count-docs.js'),
|
|
58
|
+
|
|
59
|
+
// --- Add your queries below this line ---
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// CLI runner
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
async function main(): Promise<void> {
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
const command = args[0];
|
|
69
|
+
|
|
70
|
+
// Show help
|
|
71
|
+
if (!command || command === '--help' || command === '-h') {
|
|
72
|
+
printUsage();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// List all queries (no DB connection needed for listing)
|
|
77
|
+
if (command === '--list' || command === '-l') {
|
|
78
|
+
await listQueries();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Find and run the query
|
|
83
|
+
const loader = queryRegistry[command];
|
|
84
|
+
if (!loader) {
|
|
85
|
+
console.error(`\n Unknown query: "${command}"\n`);
|
|
86
|
+
console.error(' Run with --list to see available queries.\n');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const db = await StrictDB.create({ uri: process.env.STRICTDB_URI! });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const mod = await loader();
|
|
94
|
+
const query = mod.default;
|
|
95
|
+
console.log(`\n Running: ${query.name}`);
|
|
96
|
+
console.log(` ${query.description}\n`);
|
|
97
|
+
await query.run(db, args.slice(1));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('\n Query failed:', err);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
} finally {
|
|
102
|
+
await db.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function printUsage(): void {
|
|
107
|
+
console.log(`
|
|
108
|
+
db-query — Test Query Master
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
npx tsx scripts/db-query.ts <query-name> [args...]
|
|
112
|
+
npx tsx scripts/db-query.ts --list
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--list, -l List all registered queries
|
|
116
|
+
--help, -h Show this help message
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
npx tsx scripts/db-query.ts example-find-user test@example.com
|
|
120
|
+
npx tsx scripts/db-query.ts example-count-docs users
|
|
121
|
+
npx tsx scripts/db-query.ts --list
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function listQueries(): Promise<void> {
|
|
126
|
+
console.log('\n Available queries:\n');
|
|
127
|
+
|
|
128
|
+
for (const [name, loader] of Object.entries(queryRegistry)) {
|
|
129
|
+
try {
|
|
130
|
+
const mod = await loader();
|
|
131
|
+
console.log(` ${name.padEnd(30)} ${mod.default.description}`);
|
|
132
|
+
} catch {
|
|
133
|
+
console.log(` ${name.padEnd(30)} (failed to load)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
main().catch((err) => {
|
|
141
|
+
console.error('Fatal error:', err);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example query: Count documents in a collection
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx scripts/db-query.ts example-count-docs <collection>
|
|
5
|
+
*
|
|
6
|
+
* This is a TEST query — not production code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { StrictDB } from 'strictdb';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
name: 'example-count-docs',
|
|
13
|
+
description: 'Count documents in any collection',
|
|
14
|
+
|
|
15
|
+
async run(db: StrictDB, args: string[]): Promise<void> {
|
|
16
|
+
const collection = args[0];
|
|
17
|
+
if (!collection) {
|
|
18
|
+
console.error(' Usage: example-count-docs <collection>');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const total = await db.count(collection);
|
|
23
|
+
console.log(` Collection "${collection}" has ${total} documents.`);
|
|
24
|
+
},
|
|
25
|
+
};
|