@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
package/README.npm.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @claude-code-mastery/starter-kit
|
|
2
|
+
|
|
3
|
+
A Claude Code toolkit that installs 27 commands, 10 skills, 10 hooks, and scaffolding templates into Claude Code globally - so every project you open already has them available.
|
|
4
|
+
|
|
5
|
+
**Requires:** Node.js 20+ and [Claude Code](https://claude.ai/code)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @claude-code-mastery/starter-kit init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's the only command you need to run once. Everything else happens inside Claude Code.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## What gets installed
|
|
20
|
+
|
|
21
|
+
After `init`, your `~/.claude/` directory looks like this:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
~/.claude/
|
|
25
|
+
├── starter-kit/ <- package files live here (single source of truth)
|
|
26
|
+
│ ├── commands/ <- 27 slash commands
|
|
27
|
+
│ ├── hooks/ <- 10 lifecycle hooks
|
|
28
|
+
│ ├── skills/ <- 10 skill files
|
|
29
|
+
│ └── .starter-kit/ <- scaffolding templates for /new-project
|
|
30
|
+
├── commands/
|
|
31
|
+
│ ├── new-project.md -> (symlink to starter-kit/commands/new-project.md)
|
|
32
|
+
│ ├── review.md -> (symlink to starter-kit/commands/review.md)
|
|
33
|
+
│ └── ... -> one symlink per command
|
|
34
|
+
├── skills/
|
|
35
|
+
│ ├── code-review/ -> (symlink to starter-kit/skills/code-review/)
|
|
36
|
+
│ └── ...
|
|
37
|
+
└── settings.json <- hooks registered with absolute paths
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Commands and skills are **symlinked**, not copied. When you update the package, the symlinks point to the new files immediately - no re-linking needed.
|
|
41
|
+
|
|
42
|
+
Hooks are registered by absolute path in `~/.claude/settings.json`, which is the same pattern Claude Code uses for its own global configuration.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
Once installed, these slash commands are available in every Claude Code session:
|
|
49
|
+
|
|
50
|
+
| Command | What it does |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `/new-project <name> [profile]` | Scaffold a new project with full Claude Code tooling |
|
|
53
|
+
| `/convert-project-to-starter-kit` | Add kit infrastructure to an existing project (non-destructive) |
|
|
54
|
+
| `/add-feature <capability>` | Add MongoDB, Docker, testing, etc. to an existing project |
|
|
55
|
+
| `/review` | Review code for bugs, security issues, and best practices |
|
|
56
|
+
| `/commit` | Generate a conventional commit message from staged changes |
|
|
57
|
+
| `/create-api <resource>` | Scaffold a full API endpoint with route, handler, types, and tests |
|
|
58
|
+
| `/create-e2e <feature>` | Create a Playwright E2E test with explicit success criteria |
|
|
59
|
+
| `/refactor <file>` | Refactor a file following project best practices |
|
|
60
|
+
| `/security-check` | Scan for exposed secrets, missing .gitignore entries, unsafe patterns |
|
|
61
|
+
| `/optimize-docker` | Audit a Dockerfile against 12 best practices |
|
|
62
|
+
| `/diagram <type>` | Generate architecture, API, database, or infrastructure diagrams |
|
|
63
|
+
| `/worktree <name>` | Create an isolated git worktree and branch for a task |
|
|
64
|
+
| `/setup` | Configure .env, GitHub, Docker, analytics, and services interactively |
|
|
65
|
+
| `/test-plan <feature>` | Generate a structured test plan |
|
|
66
|
+
| `/progress` | Show what's done, pending, and next in the project |
|
|
67
|
+
| `/starter-kit update` | Update to the latest version from npm |
|
|
68
|
+
| `/starter-kit status` | Show installed version vs latest |
|
|
69
|
+
| `/starter-kit add` | Install the kit into the current project's .claude/ directory |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Skills
|
|
74
|
+
|
|
75
|
+
Skills are expertise files Claude loads when relevant. These install automatically:
|
|
76
|
+
|
|
77
|
+
- `code-review` - security, performance, and correctness checks with severity rankings
|
|
78
|
+
- `test-writer` - tests with explicit assertions and realistic data
|
|
79
|
+
- `debugger` - root cause diagnosis for crashes, stack traces, and intermittent bugs
|
|
80
|
+
- `dependency-vetting` - supply-chain risk assessment before adding a package
|
|
81
|
+
- `design-review` - usability, accessibility, and visual feedback on UI
|
|
82
|
+
- `mongodb-rules` - native-driver and StrictDB rules for MongoDB data access
|
|
83
|
+
- `api-conventions` - routing and layering conventions for service code
|
|
84
|
+
- `create-service` - scaffolding patterns for new services
|
|
85
|
+
- `mcp-builder` - MCP server development patterns
|
|
86
|
+
- `terminal-tui` - Ink + React TUI patterns, resize handling
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Hooks
|
|
91
|
+
|
|
92
|
+
Hooks fire automatically during Claude Code sessions:
|
|
93
|
+
|
|
94
|
+
| Hook | When it fires | What it does |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `check-branch.sh` | Before commit | Blocks commits directly to main |
|
|
97
|
+
| `check-rybbit.sh` | Before deploy commands | Ensures analytics are configured |
|
|
98
|
+
| `check-ports.sh` | Before starting servers | Checks for port conflicts |
|
|
99
|
+
| `check-e2e.sh` | Before E2E tests | Validates test configuration |
|
|
100
|
+
| `check-env-sync.sh` | After editing .env | Flags when .env.example is out of sync |
|
|
101
|
+
| `check-rulecatch.sh` | After commits | Runs RuleCatch violation checks |
|
|
102
|
+
| `block-dangerous-bash.py` | Before Bash tool | Blocks destructive shell commands |
|
|
103
|
+
| `check-file-length.py` | After Write/Edit | Warns when files exceed 300 lines |
|
|
104
|
+
| `lint-on-save.sh` | After Write/Edit | Runs linter on changed files |
|
|
105
|
+
| `verify-no-secrets.sh` | Before commit | Scans staged files for secrets |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## How Claude Code discovers the installed content
|
|
110
|
+
|
|
111
|
+
Claude Code does not scan `node_modules/`. Instead it looks in specific directories:
|
|
112
|
+
|
|
113
|
+
| Content type | Where Claude Code looks |
|
|
114
|
+
|---|---|
|
|
115
|
+
| Commands | `~/.claude/commands/` and `<project>/.claude/commands/` |
|
|
116
|
+
| Skills | `~/.claude/skills/` and `<project>/.claude/skills/` |
|
|
117
|
+
| Hooks | Absolute paths registered in `~/.claude/settings.json` |
|
|
118
|
+
|
|
119
|
+
After `init`, the package files live in `~/.claude/starter-kit/` and are exposed via symlinks and hook registrations in exactly the places Claude Code already checks. No configuration changes to Claude Code are needed.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Updating
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx @claude-code-mastery/starter-kit update
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or from inside Claude Code after the first install:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
/starter-kit update
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This overwrites `~/.claude/starter-kit/` with the latest package content. Because commands and skills are symlinked, they reflect the update without any re-linking step.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Adding to a specific project
|
|
140
|
+
|
|
141
|
+
To give a project its own local copy of the commands and hooks (useful for team repos):
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
/starter-kit add
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This copies from `~/.claude/starter-kit/` into the current project's `.claude/` directory. Existing files are not overwritten.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Checking what's installed
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npx @claude-code-mastery/starter-kit status
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or from Claude Code:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
/starter-kit status
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Profiles for /new-project
|
|
166
|
+
|
|
167
|
+
The `/new-project` command uses profiles to scaffold different project types:
|
|
168
|
+
|
|
169
|
+
| Profile | What it creates |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `clean` | Minimal structure - just the Claude Code tooling, no framework |
|
|
172
|
+
| `node` (default) | Node.js + TypeScript, Express or similar, StrictDB |
|
|
173
|
+
| `go` | Go project with standard layout, golangci-lint, Makefile |
|
|
174
|
+
| `python` | Python + FastAPI or Django, pytest, ruff, Pydantic |
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Examples inside Claude Code:
|
|
178
|
+
/new-project my-api node
|
|
179
|
+
/new-project my-service go
|
|
180
|
+
/new-project my-site clean
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Source and contributing
|
|
186
|
+
|
|
187
|
+
The package content lives in the starter kit repo:
|
|
188
|
+
[github.com/TheDecipherist/claude-code-mastery-project-starter-kit](https://github.com/TheDecipherist/claude-code-mastery-project-starter-kit)
|
|
189
|
+
|
|
190
|
+
Clone the repo to customize commands, hooks, or skills for your own team, or fork it and publish under your own org.
|
package/bin/cli.js
CHANGED
|
@@ -80,7 +80,29 @@ export function rewriteHookPath(command, starterKitDir) {
|
|
|
80
80
|
export function copyPackageFiles(homeDir = os.homedir()) {
|
|
81
81
|
const { starterKitDir } = paths(homeDir);
|
|
82
82
|
ensureDir(starterKitDir);
|
|
83
|
+
// Copy .claude/ subdirectory contents (commands, hooks, skills, agents, .starter-kit, settings.json)
|
|
83
84
|
fs.cpSync(path.join(PKG_ROOT, '.claude'), starterKitDir, { recursive: true, force: true });
|
|
85
|
+
// Copy root template files that command files reference via $SOURCE/<file>
|
|
86
|
+
const rootTemplates = [
|
|
87
|
+
'CLAUDE.md',
|
|
88
|
+
'.env.example',
|
|
89
|
+
'.gitignore',
|
|
90
|
+
'.dockerignore',
|
|
91
|
+
'vitest.config.ts',
|
|
92
|
+
'playwright.config.ts',
|
|
93
|
+
'tsconfig.json',
|
|
94
|
+
'scripts',
|
|
95
|
+
];
|
|
96
|
+
for (const name of rootTemplates) {
|
|
97
|
+
const src = path.join(PKG_ROOT, name);
|
|
98
|
+
if (!fs.existsSync(src)) continue;
|
|
99
|
+
const dest = path.join(starterKitDir, name);
|
|
100
|
+
if (fs.statSync(src).isDirectory()) {
|
|
101
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
102
|
+
} else {
|
|
103
|
+
fs.copyFileSync(src, dest);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
84
106
|
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
85
107
|
fs.writeFileSync(path.join(starterKitDir, 'version'), pkg.version, 'utf8');
|
|
86
108
|
console.log(` Copied package files → ${starterKitDir}`);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-code-mastery/starter-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Production-ready Claude Code starter kit — commands, hooks, skills, and agents for AI-assisted development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"
|
|
10
|
+
"starter-kit": "bin/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
".claude/commands/",
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
".claude/settings.json",
|
|
19
19
|
"bin/",
|
|
20
20
|
"claude-mastery-project.conf",
|
|
21
|
-
"global-claude-md/"
|
|
21
|
+
"global-claude-md/",
|
|
22
|
+
"CLAUDE.md",
|
|
23
|
+
".env.example",
|
|
24
|
+
".gitignore",
|
|
25
|
+
".dockerignore",
|
|
26
|
+
"vitest.config.ts",
|
|
27
|
+
"playwright.config.ts",
|
|
28
|
+
"tsconfig.json",
|
|
29
|
+
"scripts/"
|
|
22
30
|
],
|
|
23
31
|
"scripts": {
|
|
24
32
|
"dev": "tsx watch src/index.ts",
|
|
@@ -53,7 +61,9 @@
|
|
|
53
61
|
"ai:monitor": "npx @rulecatch/ai-pooler monitor --no-api-key",
|
|
54
62
|
"docker:optimize": "echo 'Run /optimize-docker in Claude Code to audit your Dockerfile'",
|
|
55
63
|
"clean": "rm -rf dist coverage test-results playwright-report",
|
|
56
|
-
"precommit": "tsc --noEmit"
|
|
64
|
+
"precommit": "tsc --noEmit",
|
|
65
|
+
"prepack": "mv README.md _gh_readme.md && cp README.npm.md README.md",
|
|
66
|
+
"postpack": "mv _gh_readme.md README.md"
|
|
57
67
|
},
|
|
58
68
|
"engines": {
|
|
59
69
|
"node": ">=20.0.0"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright E2E Test Configuration
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: E2E tests run on TEST PORTS, not dev ports.
|
|
5
|
+
* This prevents conflicts with running dev servers.
|
|
6
|
+
*
|
|
7
|
+
* Test Ports (from CLAUDE.md — NEVER CHANGE):
|
|
8
|
+
* Website: 4000
|
|
9
|
+
* API: 4010
|
|
10
|
+
* Dashboard: 4020
|
|
11
|
+
*
|
|
12
|
+
* Dev Ports (for development — NOT used in tests):
|
|
13
|
+
* Website: 3000
|
|
14
|
+
* API: 3001
|
|
15
|
+
* Dashboard: 3002
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
testDir: './tests/e2e',
|
|
22
|
+
fullyParallel: true,
|
|
23
|
+
forbidOnly: !!process.env.CI,
|
|
24
|
+
retries: process.env.CI ? 2 : 0,
|
|
25
|
+
workers: process.env.CI ? 1 : undefined,
|
|
26
|
+
reporter: [
|
|
27
|
+
['html', { outputFolder: 'playwright-report' }],
|
|
28
|
+
['list'],
|
|
29
|
+
],
|
|
30
|
+
outputDir: 'test-results',
|
|
31
|
+
|
|
32
|
+
use: {
|
|
33
|
+
/* Base URL for E2E tests — uses TEST port, not dev port */
|
|
34
|
+
baseURL: 'http://localhost:4000',
|
|
35
|
+
trace: 'on-first-retry',
|
|
36
|
+
screenshot: 'only-on-failure',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
projects: [
|
|
40
|
+
{
|
|
41
|
+
name: 'chromium',
|
|
42
|
+
use: { ...devices['Desktop Chrome'] },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'firefox',
|
|
46
|
+
use: { ...devices['Desktop Firefox'] },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'webkit',
|
|
50
|
+
use: { ...devices['Desktop Safari'] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'mobile-chrome',
|
|
54
|
+
use: { ...devices['Pixel 5'] },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
/* Start services on TEST ports before running E2E tests */
|
|
59
|
+
webServer: [
|
|
60
|
+
{
|
|
61
|
+
command: 'pnpm dev:test:website',
|
|
62
|
+
port: 4000,
|
|
63
|
+
reuseExistingServer: !process.env.CI,
|
|
64
|
+
timeout: 30_000,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
command: 'pnpm dev:test:api',
|
|
68
|
+
port: 4010,
|
|
69
|
+
reuseExistingServer: !process.env.CI,
|
|
70
|
+
timeout: 30_000,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
command: 'pnpm dev:test:dashboard',
|
|
74
|
+
port: 4020,
|
|
75
|
+
reuseExistingServer: !process.env.CI,
|
|
76
|
+
timeout: 30_000,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
package/scripts/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* build-content.ts — Markdown-to-HTML Article Builder (CLI)
|
|
4
|
+
*
|
|
5
|
+
* Converts markdown files to fully SEO-ready static HTML pages using a
|
|
6
|
+
* JSON config file as the single source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Based on the production article builder from TheDecipherist.
|
|
9
|
+
* https://github.com/TheDecipherist/claude-code-mastery
|
|
10
|
+
*
|
|
11
|
+
* USAGE:
|
|
12
|
+
* npx tsx scripts/build-content.ts # Build all published
|
|
13
|
+
* npx tsx scripts/build-content.ts --id getting-started # Build one article
|
|
14
|
+
* npx tsx scripts/build-content.ts --list # List all articles
|
|
15
|
+
* npx tsx scripts/build-content.ts --dry-run # Show what would build
|
|
16
|
+
*
|
|
17
|
+
* CONFIG:
|
|
18
|
+
* Edit scripts/content.config.json to add/modify articles.
|
|
19
|
+
* Each article needs: id, mdSource, htmlOutput, title, description, url
|
|
20
|
+
*
|
|
21
|
+
* MODULES:
|
|
22
|
+
* scripts/content/types.ts — Types & Zod validation
|
|
23
|
+
* scripts/content/markdown-processor.ts — Markdown → HTML conversion
|
|
24
|
+
* scripts/content/seo-generator.ts — Schema.org JSON-LD
|
|
25
|
+
* scripts/content/sidebar-generator.ts — Sidebar TOC & navigation
|
|
26
|
+
* scripts/content/html-template.ts — Full HTML page assembly
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { ContentConfigSchema } from './content/types.js';
|
|
32
|
+
import { buildArticle } from './content/html-template.js';
|
|
33
|
+
|
|
34
|
+
function main(): void {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const configPath = path.resolve(import.meta.dirname ?? __dirname, 'content.config.json');
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(configPath)) {
|
|
39
|
+
console.error(`Config not found: ${configPath}`);
|
|
40
|
+
console.error('Create scripts/content.config.json first.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const raw: unknown = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
45
|
+
const parsed = ContentConfigSchema.safeParse(raw);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
console.error('\n Invalid content.config.json:\n');
|
|
48
|
+
for (const issue of parsed.error.issues) {
|
|
49
|
+
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
|
50
|
+
}
|
|
51
|
+
console.error('');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const config = parsed.data;
|
|
55
|
+
|
|
56
|
+
const published = config.articles.filter((a) => a.published);
|
|
57
|
+
|
|
58
|
+
// --list
|
|
59
|
+
if (args.includes('--list')) {
|
|
60
|
+
console.log(`\n ${config.articles.length} articles (${published.length} published):\n`);
|
|
61
|
+
for (const a of config.articles) {
|
|
62
|
+
const status = a.published ? '\u2713' : '\u25CB';
|
|
63
|
+
console.log(` ${status} ${a.id.padEnd(35)} ${a.category ?? ''}`);
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --dry-run
|
|
70
|
+
if (args.includes('--dry-run')) {
|
|
71
|
+
console.log(`\n Would build ${published.length} articles:\n`);
|
|
72
|
+
for (const a of published) {
|
|
73
|
+
console.log(` ${a.mdSource} \u2192 ${a.htmlOutput}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --id <article-id>
|
|
80
|
+
const idIdx = args.indexOf('--id');
|
|
81
|
+
if (idIdx !== -1) {
|
|
82
|
+
const targetId = args[idIdx + 1];
|
|
83
|
+
const article = config.articles.find((a) => a.id === targetId);
|
|
84
|
+
if (!article) {
|
|
85
|
+
console.error(`Article not found: ${targetId}`);
|
|
86
|
+
console.error('Run with --list to see available articles.');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
console.log(`\n Building: ${article.id}\n`);
|
|
90
|
+
buildArticle(article, config);
|
|
91
|
+
console.log('\n Done.\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Default: build all published
|
|
96
|
+
if (published.length === 0) {
|
|
97
|
+
console.log('\n No published articles to build.\n');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`\n Building ${published.length} published article(s)...\n`);
|
|
102
|
+
for (const article of published) {
|
|
103
|
+
buildArticle(article, config);
|
|
104
|
+
}
|
|
105
|
+
console.log(`\n Done. ${published.length} articles built.\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Builder — HTML Page Template
|
|
3
|
+
*
|
|
4
|
+
* Assembles final HTML pages from processed content, SEO data, and sidebar.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import type { ArticleConfig, ContentConfig, CollectedHeading } from './types.js';
|
|
10
|
+
import { convertMarkdownToHtml } from './markdown-processor.js';
|
|
11
|
+
import { generateSchemaJson } from './seo-generator.js';
|
|
12
|
+
import { generateSidebarHtml } from './sidebar-generator.js';
|
|
13
|
+
|
|
14
|
+
export function buildArticle(article: ArticleConfig, config: ContentConfig): void {
|
|
15
|
+
const markdown = fs.readFileSync(article.mdSource, 'utf8').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
16
|
+
|
|
17
|
+
const headings: CollectedHeading[] = [];
|
|
18
|
+
|
|
19
|
+
// Remove first H1 (shown in header)
|
|
20
|
+
const processedMd = markdown.replace(/^# .*\n+/, '');
|
|
21
|
+
const articleContent = convertMarkdownToHtml(processedMd.trim(), headings);
|
|
22
|
+
|
|
23
|
+
const sidebarHtml = generateSidebarHtml(article, headings);
|
|
24
|
+
const hasSidebar = article.sidebar === true;
|
|
25
|
+
|
|
26
|
+
const mainSection = hasSidebar
|
|
27
|
+
? ` <div class="article-layout">
|
|
28
|
+
${sidebarHtml}
|
|
29
|
+
<main>
|
|
30
|
+
<article class="article-content">
|
|
31
|
+
${articleContent}
|
|
32
|
+
</article>
|
|
33
|
+
</main>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
|
36
|
+
<button class="sidebar-toggle" id="sidebarToggle">Contents</button>`
|
|
37
|
+
: ` <main>
|
|
38
|
+
<article class="article-content">
|
|
39
|
+
${articleContent}
|
|
40
|
+
</article>
|
|
41
|
+
</main>`;
|
|
42
|
+
|
|
43
|
+
const sidebarJs = hasSidebar ? generateSidebarJs() : '';
|
|
44
|
+
|
|
45
|
+
const fullHtml = `<!DOCTYPE html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
50
|
+
<title>${article.title} — ${config.siteName}</title>
|
|
51
|
+
<meta name="description" content="${article.description}">
|
|
52
|
+
<meta name="author" content="${config.author}">
|
|
53
|
+
<meta name="robots" content="index, follow">
|
|
54
|
+
<link rel="canonical" href="${article.url}">
|
|
55
|
+
|
|
56
|
+
<!-- Open Graph / Facebook -->
|
|
57
|
+
<meta property="og:type" content="article">
|
|
58
|
+
<meta property="og:url" content="${article.url}">
|
|
59
|
+
<meta property="og:title" content="${article.title} — ${config.siteName}">
|
|
60
|
+
<meta property="og:description" content="${article.description}">
|
|
61
|
+
${article.bannerImage ? `<meta property="og:image" content="${config.siteUrl}${article.bannerImage}">` : ''}
|
|
62
|
+
<meta property="og:site_name" content="${config.siteName}">
|
|
63
|
+
|
|
64
|
+
<!-- Twitter -->
|
|
65
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
66
|
+
<meta name="twitter:title" content="${article.title} — ${config.siteName}">
|
|
67
|
+
<meta name="twitter:description" content="${article.description}">
|
|
68
|
+
${article.bannerImage ? `<meta name="twitter:image" content="${config.siteUrl}${article.bannerImage}">` : ''}
|
|
69
|
+
|
|
70
|
+
<!-- Schema.org -->
|
|
71
|
+
${generateSchemaJson(article, config)}
|
|
72
|
+
|
|
73
|
+
<!-- Syntax highlighting -->
|
|
74
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
|
75
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
76
|
+
|
|
77
|
+
<!-- Add your CSS here -->
|
|
78
|
+
<link rel="stylesheet" href="/css/global.css">
|
|
79
|
+
<link rel="stylesheet" href="/css/article.css">
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<header>
|
|
83
|
+
<h1>${article.titleHtml ?? article.title}</h1>
|
|
84
|
+
${article.subtitle ? `<p class="subtitle">${article.subtitle}</p>` : ''}
|
|
85
|
+
</header>
|
|
86
|
+
|
|
87
|
+
${article.bannerImage ? `<div class="hero-banner" role="img" aria-label="${article.bannerAlt ?? article.title}" style="background-image: url('${article.bannerImage}')"></div>` : ''}
|
|
88
|
+
|
|
89
|
+
${mainSection}
|
|
90
|
+
|
|
91
|
+
<script>
|
|
92
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
93
|
+
document.querySelectorAll('pre code').forEach(function(block) {
|
|
94
|
+
hljs.highlightElement(block);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
${sidebarJs}
|
|
98
|
+
</script>
|
|
99
|
+
</body>
|
|
100
|
+
</html>`;
|
|
101
|
+
|
|
102
|
+
const outputDir = path.dirname(article.htmlOutput);
|
|
103
|
+
if (!fs.existsSync(outputDir)) {
|
|
104
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(article.htmlOutput, fullHtml);
|
|
108
|
+
const sizeKb = (fullHtml.length / 1024).toFixed(1);
|
|
109
|
+
console.log(` \u2713 ${article.id} \u2192 ${article.htmlOutput} (${sizeKb} KB, ${headings.length} headings)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function generateSidebarJs(): string {
|
|
113
|
+
return `
|
|
114
|
+
// Sidebar scroll spy
|
|
115
|
+
(function() {
|
|
116
|
+
var tocLinks = document.querySelectorAll('.sidebar-toc a');
|
|
117
|
+
var headings = [];
|
|
118
|
+
tocLinks.forEach(function(link) {
|
|
119
|
+
var id = link.getAttribute('href').slice(1);
|
|
120
|
+
var heading = document.getElementById(id);
|
|
121
|
+
if (heading) headings.push({ el: heading, link: link });
|
|
122
|
+
});
|
|
123
|
+
if (headings.length > 0) {
|
|
124
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
125
|
+
entries.forEach(function(entry) {
|
|
126
|
+
if (entry.isIntersecting) {
|
|
127
|
+
tocLinks.forEach(function(l) { l.classList.remove('active'); });
|
|
128
|
+
var match = headings.find(function(h) { return h.el === entry.target; });
|
|
129
|
+
if (match) match.link.classList.add('active');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}, { rootMargin: '-80px 0px -70% 0px' });
|
|
133
|
+
headings.forEach(function(h) { observer.observe(h.el); });
|
|
134
|
+
}
|
|
135
|
+
var sidebar = document.getElementById('articleSidebar');
|
|
136
|
+
var overlay = document.getElementById('sidebarOverlay');
|
|
137
|
+
var toggle = document.getElementById('sidebarToggle');
|
|
138
|
+
function openSidebar() { if (sidebar) sidebar.classList.add('open'); if (overlay) overlay.classList.add('open'); }
|
|
139
|
+
function closeSidebar() { if (sidebar) sidebar.classList.remove('open'); if (overlay) overlay.classList.remove('open'); }
|
|
140
|
+
if (toggle) toggle.addEventListener('click', openSidebar);
|
|
141
|
+
if (overlay) overlay.addEventListener('click', closeSidebar);
|
|
142
|
+
tocLinks.forEach(function(link) { link.addEventListener('click', closeSidebar); });
|
|
143
|
+
})();`;
|
|
144
|
+
}
|