@ibalzam/codejitsu-core 0.10.0 → 0.11.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/bin/codejitsu.mjs CHANGED
@@ -3,6 +3,7 @@ import { parseArgs } from 'node:util';
3
3
  import { runBlog } from '../modules/cli/src/blog.mjs';
4
4
  import { runDeploySetup, runDeployTrigger } from '../modules/cli/src/deploy.mjs';
5
5
  import { runDoctor } from '../modules/cli/src/doctor.mjs';
6
+ import { runBlogInit } from '../modules/cli/src/blog-init.mjs';
6
7
  import { runAudit } from '../modules/audit/src/run.mjs';
7
8
 
8
9
  const subcommand = process.argv[2];
@@ -11,6 +12,7 @@ const rest = process.argv.slice(3);
11
12
  const COMMANDS = {
12
13
  'blog:list': () => runBlog('blog:list'),
13
14
  'blog:drafts': () => runBlog('blog:drafts'),
15
+ 'blog:init': () => runBlogInit(),
14
16
  'deploy:setup': () => runDeploySetup(),
15
17
  'deploy:run': () => runDeployTrigger(),
16
18
  doctor: () => runDoctor(),
@@ -56,6 +58,7 @@ function printHelp() {
56
58
  console.log(`Subcommands:`);
57
59
  console.log(` blog:list List every non-draft post with URL + image check`);
58
60
  console.log(` blog:drafts List future-dated (pending) posts only`);
61
+ console.log(` blog:init Install /blog, /blog-batch, /blog-images slash commands`);
59
62
  console.log(``);
60
63
  console.log(` deploy:setup Wire up daily Cloudflare deploy (prompts for hook URL)`);
61
64
  console.log(` deploy:run Trigger the Daily Deploy workflow once now`);
@@ -0,0 +1,142 @@
1
+ # Blog batch generation — Claude playbook
2
+
3
+ > Triggered when the user runs `/blog-batch <N>` in a Codejitsu site
4
+ > (default N = 20).
5
+ >
6
+ > Generates a **schedule + outline** for N future-dated posts. It does NOT
7
+ > write the full body of every post — that's `/blog` per row. The goal here
8
+ > is to produce a publishable roadmap that the user reviews + edits before
9
+ > any prose is committed.
10
+
11
+ ## Step 0 — Read site config
12
+
13
+ Same as `BLOG_WRITING.md` Step 0: load `codejitsu.config.ts.blogWriter`.
14
+
15
+ Also read:
16
+ - `src/content/blog/` — existing posts (find the latest `pubDate`)
17
+ - `src/content.config.ts` — confirm the schema shape
18
+
19
+ ## Step 1 — Decide cadence
20
+
21
+ Default cadence is **4 days between posts** (matches workzen / pearl /
22
+ veteran). If existing posts have a different rhythm, infer from the last
23
+ 10 and match it.
24
+
25
+ Starting date = max(`latest existing pubDate`, today) + cadence.
26
+
27
+ ## Step 2 — Generate N topics
28
+
29
+ Brainstorm `N` topics, balancing across:
30
+
31
+ - **Service × location** combinations — cycle through `services` and
32
+ `locations` from config so coverage is even
33
+ - **Topic format** mix — roughly:
34
+ - 30% comparison / "X vs Y" posts
35
+ - 30% how-to / guides
36
+ - 20% troubleshooting
37
+ - 20% seasonal / news
38
+ - **Season awareness** — apply `seasonalRules` from config. A post landing
39
+ in July shouldn't be about "winterize"; one in December shouldn't be
40
+ about "pre-summer".
41
+ - **Unique titles** — no two posts in the batch should target the same slug
42
+ or near-duplicate keyword. Read all existing post titles before
43
+ brainstorming to avoid collisions.
44
+ - **Approved tag balance** — distribute tags so no single tag is over 40% of
45
+ the batch.
46
+
47
+ For each topic, derive:
48
+ - `slug` — kebab-case, includes the city when applicable
49
+ - `title` — natural, clickable, ≤70 chars
50
+ - `pubDate` — assigned by cadence walk
51
+ - `tags` — 2-3 from `approvedTags`, primary first
52
+ - `service` + `city` — for internal-link planning
53
+
54
+ ## Step 3 — Produce the schedule table
55
+
56
+ Write to `Blog/BLOG_SCHEDULE.md` (create the `Blog/` directory if needed).
57
+ Append to it if it already exists — never overwrite without asking.
58
+
59
+ Format:
60
+
61
+ ```markdown
62
+ # <Site Name> — Blog Schedule (<start month> - <end month> <year>)
63
+
64
+ Offline publishing calendar. N posts on a <cadence>-day cadence,
65
+ <start date> through <end date>.
66
+
67
+ ## How to use
68
+
69
+ 1. Pick a row. Slug = filename → `src/content/blog/<slug>.md`.
70
+ 2. `pubDate: <date>` in frontmatter controls visibility.
71
+ 3. Run `/blog "<title>"` to flesh out the post body.
72
+ 4. Run `/blog-images` once a batch of posts is drafted.
73
+
74
+ ## Schedule
75
+
76
+ | # | Date | Category | City | Slug | Title |
77
+ |---|------------|----------------|-----------|------|-------|
78
+ | 1 | YYYY-MM-DD | <primary tag> | <city> | `<slug>` | <Title> |
79
+ | 2 | ... | ... | ... | ... | ... |
80
+ ```
81
+
82
+ ## Step 4 — Produce per-post outlines
83
+
84
+ In the same file or a sibling `Blog/BLOG_OUTLINES.md`, expand each row into
85
+ an outline:
86
+
87
+ ```markdown
88
+ ## <Slug>
89
+
90
+ **pubDate**: YYYY-MM-DD
91
+ **target words**: <from config.wordCount.default>
92
+ **tags**: [primary, secondary]
93
+ **service**: <slug>
94
+ **city**: <slug or 'general'>
95
+
96
+ **Angle / hook**: <one-sentence framing of the reader's pain>
97
+
98
+ **Outline**:
99
+ - Intro — 1-2 paragraphs hooking <hook>
100
+ - ## <H2 #1>
101
+ - 2-3 paragraphs covering <points>
102
+ - ## <H2 #2>
103
+ - Comparison table of <X vs Y>
104
+ - ## <H2 #3>
105
+ - ...
106
+ - Closing CTA + 5-8 FAQs
107
+
108
+ **FAQ seeds**: list of 5-8 question stems
109
+ ```
110
+
111
+ ## Step 5 — Show + approve
112
+
113
+ Present the full schedule + outline file to the user. Ask:
114
+
115
+ > "Here's the schedule. Anything to adjust before we lock it in?
116
+ > Want me to flesh out the first N posts now, or stop here for review?"
117
+
118
+ If the user says go: run `/blog` for each row (one post at a time). If they
119
+ say stop: leave the file and exit. They can run `/blog` row by row whenever.
120
+
121
+ ## Hard rules
122
+
123
+ - **Don't write the full prose for all N in one shot.** Too long, too
124
+ expensive, hard to review. Schedule + outlines first; prose per row on
125
+ demand.
126
+ - **Don't push or commit.** The user reviews + commits.
127
+ - **Respect existing posts.** If `Blog/BLOG_SCHEDULE.md` already has rows,
128
+ append the new batch and don't renumber.
129
+ - **Don't invent services or locations.** Use only what's in
130
+ `blogWriter.services` and `blogWriter.locations`.
131
+ - **Don't invent tags.** Only from `approvedTags`.
132
+ - **Don't bunch up topic types.** Reject your own first draft if it has 5
133
+ comparison posts in a row.
134
+
135
+ ## Verify
136
+
137
+ - [ ] N rows, all unique slugs
138
+ - [ ] Dates respect cadence; no collisions with existing posts
139
+ - [ ] Tags balanced across `approvedTags`
140
+ - [ ] Service + city distribution covers most of the config lists
141
+ - [ ] Season fits each `pubDate`'s month
142
+ - [ ] No duplicate or near-duplicate topics
@@ -0,0 +1,127 @@
1
+ # Blog image prompts — Claude playbook
2
+
3
+ > Triggered when the user runs `/blog-images` (or `/blog-images <N>`) in a
4
+ > Codejitsu site.
5
+ >
6
+ > Generates AI-image-generation prompts for blog posts that don't have their
7
+ > images yet. Output goes to `Blog/IMAGE_PROMPTS.md` in the **5-dash separator
8
+ > format**.
9
+
10
+ ## Step 0 — Read site config
11
+
12
+ Open `codejitsu.config.ts` and locate `blogWriter.imageStyle`. Use:
13
+
14
+ - `imageStyle.description` — the full prompt-style description of the visual
15
+ style (photorealistic / cartoon / illustration / etc., palette, framing)
16
+ - `imageStyle.branding` — branding rule (e.g. "logo small in bottom-right")
17
+ - `imageStyle.outputDir` — where the final .webp will live
18
+ - `imageStyle.maxWords` — cap on prompt length (typical ≤60)
19
+ - `imageStyle.realism` — `'photorealistic' | 'illustration' | 'cartoon' | 'mixed'`
20
+
21
+ ## Step 1 — Find the posts to prompt
22
+
23
+ Default `N = 10` if not specified. Scan `src/content/blog/` and pick posts
24
+ that:
25
+
26
+ 1. Have `pubDate > today` (pending / scheduled), OR
27
+ 2. Are missing an image file at `<imageStyle.outputDir>/<slug>.webp`
28
+
29
+ Sort by `pubDate` ascending. Take up to N.
30
+
31
+ If nothing matches → tell the user "no pending posts need images" and stop.
32
+
33
+ ## Step 2 — Generate prompts
34
+
35
+ For each post, write a prompt ≤`maxWords` words that:
36
+
37
+ - Captures the **specific topic** of the post (e.g. a comparison post should
38
+ show both materials, a city-anchored post should hint at the city's
39
+ setting)
40
+ - Follows the style + branding from `imageStyle`
41
+ - References any specific materials, fixtures, or settings mentioned in the
42
+ post title
43
+ - **Doesn't** include text, captions, watermarks, or competitor brand names
44
+ - **Doesn't** specify image dimensions (the image generator decides)
45
+
46
+ If `imageStyle.realism === 'photorealistic'`, prompts emphasize architecture,
47
+ lighting, real materials, magazine-quality framing.
48
+
49
+ If `imageStyle.realism === 'cartoon'`, prompts emphasize character design,
50
+ flat colors, friendly tone.
51
+
52
+ ## Step 3 — Output format
53
+
54
+ Write to `Blog/IMAGE_PROMPTS.md` (create the `Blog/` directory if absent).
55
+ Append if the file exists — never overwrite existing prompts.
56
+
57
+ The format is **EXACTLY**:
58
+
59
+ ```
60
+ **<Post Title>**
61
+ <prompt text>
62
+
63
+ -----
64
+
65
+ **<Post Title>**
66
+ <prompt text>
67
+
68
+ -----
69
+
70
+ **<Post Title>**
71
+ <prompt text>
72
+ ```
73
+
74
+ ### Format rules — strict
75
+
76
+ - Separator is **exactly 5 dashes** on their own line: `-----`. Not 3, not 7.
77
+ Markdown's `---` is wrong here; use `-----` literally.
78
+ - Separator appears **between entries**, never after the last one.
79
+ - Title goes on its own line in **bold** (with `**...**`).
80
+ - Prompt goes directly below the title line, no blank line between them.
81
+ - Blank line before each separator, blank line after.
82
+ - No numbering.
83
+ - No dimensions in the prompt text.
84
+ - The output file goes to `imageStyle.outputDir/<slug>.webp` (note this
85
+ convention in a one-line header at the top of the file, not in each
86
+ prompt).
87
+
88
+ ## Step 4 — Header for the file
89
+
90
+ Top of `Blog/IMAGE_PROMPTS.md`:
91
+
92
+ ```markdown
93
+ # <Site Name> — Blog Image Prompts (Posts <range>)
94
+
95
+ Generated against `modules/blog-writer/BLOG_IMAGES.md` from `<Site Name>`'s
96
+ `codejitsu.config.ts.blogWriter.imageStyle`.
97
+
98
+ Style: <one-line summary derived from imageStyle.description>.
99
+ Output file convention: each image saves to `<outputDir>/<slug>.webp`.
100
+
101
+ ---
102
+ ```
103
+
104
+ (That `---` IS markdown HR — it's the only 3-dash separator in the file,
105
+ separating the header from the prompts. Everything between prompt entries is
106
+ `-----`.)
107
+
108
+ ## Hard rules
109
+
110
+ 1. **Separator is `-----` (5 dashes).** Not 3.
111
+ 2. **No numbering** of entries.
112
+ 3. **No dimensions** in the prompt text.
113
+ 4. **No competitor brand names** in any prompt.
114
+ 5. **No text / captions / watermarks inside the image** unless the style
115
+ description explicitly allows it.
116
+ 6. **Append, don't overwrite.** If the user has a prior prompts file, add new
117
+ entries below; never delete or rewrite existing prompts.
118
+ 7. **One prompt per post.** Don't generate alternatives unless asked.
119
+
120
+ ## Verify
121
+
122
+ - [ ] N prompts produced (or fewer if not enough pending posts)
123
+ - [ ] Every separator is exactly 5 dashes
124
+ - [ ] Last entry has NO trailing separator
125
+ - [ ] No prompt exceeds `maxWords`
126
+ - [ ] Each prompt references the post's specific topic + city if applicable
127
+ - [ ] Output file path is `Blog/IMAGE_PROMPTS.md`
@@ -0,0 +1,198 @@
1
+ # Blog writing — Claude playbook
2
+
3
+ > Triggered when the user runs `/blog` (or `/blog <topic>`) in a Codejitsu site
4
+ > that has the kit's commands installed (`npx codejitsu blog:init`).
5
+ >
6
+ > The slash command file is a thin reference; this file is the actual playbook.
7
+
8
+ ## Step 0 — Read site config
9
+
10
+ Open `codejitsu.config.ts` at the site root and locate the `blogWriter` block.
11
+ Everything in there is **site-specific input** for what you're about to write:
12
+
13
+ - `tone` — voice + register
14
+ - `about` — what the company does, who it serves
15
+ - `audience` — primary reader
16
+ - `services` — names that map to `/services/<slug>/`
17
+ - `locations` — names that map to `/service-areas/<slug>/`
18
+ - `approvedTags` — exhaustive; never invent new ones
19
+ - `wordCount` — `{ min, max, default }`
20
+ - `faqs` — `{ min, max }` (kit default 5-8)
21
+ - `internalLinks` — `{ min, max }` per post
22
+ - `pricing` — `'brackets-only' | 'allowed' | 'never-mention'`
23
+ - `seasonalRules` — free text; current date should respect this
24
+ - `bannedPhrases` — refuse to write these
25
+ - `authorDefault` — what to put in frontmatter `author`
26
+
27
+ Also detect the blog's frontmatter shape by reading `src/content.config.ts` and
28
+ ONE existing post in `src/content/blog/`. Use that exact shape for the new post.
29
+
30
+ ## Step 1 — Gather inputs (interactive)
31
+
32
+ Use `AskUserQuestion` to gather these in order. If the user passed a topic as
33
+ `$ARGUMENTS`, skip Question 1.
34
+
35
+ ### Question 1 — Topic & focus
36
+
37
+ ```
38
+ header: "Topic"
39
+ question: "What is this blog post about?"
40
+ options:
41
+ - "Industry guide" — Educational content the audience would search for
42
+ - "How-to / tutorial" — Step-by-step instructions
43
+ - "Comparison / vs" — Compare 2+ options on multiple attributes
44
+ - "Troubleshooting" — Diagnose-and-fix walkthrough
45
+ - "Seasonal / news" — Time-sensitive (rebate change, season prep, etc.)
46
+ ```
47
+
48
+ ### Question 2 — Length
49
+
50
+ ```
51
+ header: "Length"
52
+ question: "How long?"
53
+ options:
54
+ - "Short (400-600 words)" — Announcement, quick explainer
55
+ - "Medium (800-1200 words)" — Single-question explainer
56
+ - "Long (1500-2500 words)" — Default for SEO posts
57
+ - "XL (2500+ words)" — Definitive guide
58
+ ```
59
+
60
+ Default = the `wordCount.default` from config.
61
+
62
+ ### Question 3 — Primary service + city focus
63
+
64
+ ```
65
+ header: "Service + city"
66
+ question: "Which service and city is this anchored to?"
67
+ ```
68
+
69
+ Two follow-ups (separate questions):
70
+ - Service options: derive from `blogWriter.services`
71
+ - City options: derive from `blogWriter.locations` (plus an "any/general" option)
72
+
73
+ These drive the internal-link choices later.
74
+
75
+ ### Question 4 — Publish date
76
+
77
+ ```
78
+ header: "Publish date"
79
+ question: "When should this go live?"
80
+ options:
81
+ - "Today" — immediate publish
82
+ - "Next available slot" — read recent posts in src/content/blog/, find the
83
+ next open date in the existing cadence (e.g. 4-day gap), use that
84
+ - "Custom date" — user types YYYY-MM-DD
85
+ ```
86
+
87
+ ## Step 2 — Outline + approval
88
+
89
+ Before writing prose, produce an outline:
90
+
91
+ ```
92
+ TITLE: <working title>
93
+ SLUG: <kebab-case>
94
+ DATE: <YYYY-MM-DD>
95
+ TAGS: <2-3 from approvedTags, primary first>
96
+ WORDS: <target word count>
97
+
98
+ OUTLINE:
99
+ Intro (1-2 paragraphs)
100
+ ## <H2 #1>
101
+ 2-3 paragraphs of [angle]
102
+ ## <H2 #2>
103
+ Includes a comparison table / bullet list of [items]
104
+ ## <H2 #3>
105
+ ...
106
+ Closing CTA (modal + phone)
107
+ FAQs (5-8): planned questions
108
+ ```
109
+
110
+ Show the outline and **ask for approval before writing prose**. Adjust on
111
+ feedback. Don't proceed without approval.
112
+
113
+ ## Step 3 — Write the post
114
+
115
+ Write the full post to `src/content/blog/<slug>.md`. Match the existing
116
+ frontmatter shape exactly (detected in Step 0).
117
+
118
+ ### HARD RULES — apply to every post
119
+
120
+ 1. **No em dashes anywhere.** Use ` - ` (regular dash with spaces around it).
121
+ 2. **No H1 in markdown body.** Frontmatter `title` becomes the H1. Body starts
122
+ with intro paragraph(s), then `## H2`.
123
+ 3. **No "one-line H2 sections."** Every `## H2` must have at least
124
+ **2-3 paragraphs**, not just a sentence + bullet list.
125
+ 4. **At least one list per post.** Either a bullet list (3+ parallel items) or
126
+ a numbered list (if order matters) or a markdown table (for comparisons).
127
+ No exceptions — a wall-of-prose 2,000-word post is wrong. But also no
128
+ slide-deck (every section ending in bullets).
129
+ 5. **FAQ block** in frontmatter — `faqs.min` to `faqs.max` entries from config
130
+ (default 5-8). Each FAQ = real question a reader would search; answer is
131
+ 2-4 sentences, concise and complete.
132
+ 6. **Internal links** — `internalLinks.min` to `internalLinks.max` from config
133
+ (default 3-6). Link to `/services/<slug>/`, `/service-areas/<slug>/`, and
134
+ `/services/<slug>/<city>/` patterns. Don't link the same URL twice.
135
+ 7. **Banned phrases** — if `bannedPhrases` is set in config, refuse them.
136
+ Common offenders: "In today's fast-paced world…", "When it comes to…",
137
+ "Look no further…", "In conclusion…".
138
+ 8. **Pricing** — if `pricing: 'brackets-only'`, every price reference is a
139
+ range with context. Never a single dollar figure. If `pricing: 'never-mention'`,
140
+ omit pricing entirely.
141
+ 9. **CTA** — closing paragraph includes a call-to-action. Two patterns:
142
+ - Modal: `[text](#contact)` or trigger-class link
143
+ - Phone: `[text](tel:+1...)` — use the actual number from `site.business.telephone`
144
+ Best is one paragraph with both.
145
+ 10. **Approved tags only.** Pick 2-3 from `blogWriter.approvedTags`. Primary
146
+ tag first. Don't invent new tags. If nothing fits, ask the user.
147
+ 11. **Image placeholder.** Frontmatter `image` field points to where the image
148
+ WILL live: `<imageStyle.outputDir>/<slug>.webp`. The file doesn't exist
149
+ yet; that's fine. Run `/blog-images` to generate prompts later.
150
+ 12. **Seasonal awareness.** If `seasonalRules` is set, the topic + angle must
151
+ fit the post's publish month. No "winterize" posts in July.
152
+
153
+ ### Structure
154
+
155
+ - **Intro** — 1-2 paragraphs that hook on a real audience pain. Tie to a
156
+ specific local concern when possible (the climate, regulation, market).
157
+ - **3-6 H2 sections** — each 2-3 paragraphs minimum. Mix prose, lists, and
158
+ tables. Roughly:
159
+ - Prose carries narrative + explanation in most sections
160
+ - 1-3 bullet lists where content is genuinely parallel items
161
+ - A markdown table for 2+ option comparisons (when it's a vs/comparison post)
162
+ - Numbered list only when order matters
163
+ - **H3 sub-sections** within H2s where useful.
164
+ - **Closing paragraph** with CTA.
165
+ - **FAQ block** in frontmatter.
166
+
167
+ ### Tone
168
+
169
+ Read `blogWriter.tone` and `blogWriter.audience` from config. Match
170
+ exactly. Examples:
171
+
172
+ - "professional but friendly, confident not boastful" → don't oversell, don't joke
173
+ - "BC HVAC pro, plain-spoken, technical when needed" → mix terms-of-art with plain explanations
174
+ - "Zen calm, helpful, occasional humour" → workzen's signature
175
+
176
+ ## Step 4 — Verify after writing
177
+
178
+ - [ ] Word count is within target range
179
+ - [ ] FAQ count is within `faqs.min`-`faqs.max`
180
+ - [ ] Internal links count is within `internalLinks.min`-`internalLinks.max`
181
+ - [ ] Tags are from `approvedTags` only
182
+ - [ ] No em dashes (`—` or `–`) anywhere
183
+ - [ ] No H1 in body
184
+ - [ ] No `bannedPhrases` present
185
+ - [ ] Pricing follows `pricing` policy
186
+ - [ ] Frontmatter shape matches existing posts
187
+ - [ ] At least one list (bullet, numbered, or table)
188
+
189
+ ## Step 5 — Report back
190
+
191
+ Tell the user:
192
+ - Path to the new file
193
+ - Word count
194
+ - Tag selection + why
195
+ - Whether image needs generating (point at `/blog-images`)
196
+ - Whether the post is published immediately or scheduled (based on date)
197
+
198
+ Don't push or commit unless explicitly asked.
@@ -0,0 +1,125 @@
1
+ # Blog writer module — instructions for Claude
2
+
3
+ When the user asks to **set up blog writing** or **add the codejitsu blog
4
+ writer** to a site, do the following.
5
+
6
+ ## What this module provides
7
+
8
+ A robust blog-writing workflow driven by per-site config + three slash
9
+ commands in the user's `.claude/commands/`. The slash commands themselves
10
+ are **thin references** to playbooks that live in the package — so updates
11
+ flow through `npm update` without re-copying anything.
12
+
13
+ **Files in this module:**
14
+
15
+ - `BLOG_WRITING.md` — single-post playbook (driven by `/blog`)
16
+ - `BLOG_BATCH.md` — schedule + outline N posts (driven by `/blog-batch <N>`)
17
+ - `BLOG_IMAGES.md` — generate image prompts for pending posts (driven by `/blog-images`)
18
+ - `templates/.claude/commands/*.md` — thin slash command files that the
19
+ site keeps in its repo (copied once via `codejitsu blog:init`)
20
+
21
+ **Config block in `codejitsu.config.ts`:**
22
+
23
+ ```ts
24
+ blogWriter: {
25
+ tone: 'professional, plain-spoken, confident not boastful',
26
+ about: 'Veteran Heating and Cooling is a Chilliwack HVAC contractor...',
27
+ audience: 'BC Lower Mainland homeowners planning HVAC upgrades',
28
+ services: ['Heat Pump Installation', 'Furnace Installation', ...],
29
+ locations: ['Vancouver', 'Burnaby', 'Surrey', ...],
30
+ approvedTags: ['rebates-savings', 'buying-guides', 'troubleshooting', 'seasonal-care', 'home-comfort'],
31
+ wordCount: { min: 1200, max: 2500, default: 1800 },
32
+ faqs: { min: 5, max: 8 },
33
+ internalLinks: { min: 3, max: 6 },
34
+ pricing: 'brackets-only',
35
+ seasonalRules: 'July-Sep: heat-wave + AC topics. Oct-Nov: pre-winter prep...',
36
+ bannedPhrases: ["In today's fast-paced world", "When it comes to", "Look no further"],
37
+ authorDefault: 'Veteran HVAC',
38
+ imageStyle: {
39
+ description: 'Photorealistic real-estate / architectural photography of HVAC equipment + BC home interiors. Bright natural daylight, modern Lower Mainland aesthetic, no people unless they wear branded uniform.',
40
+ branding: 'Logo small in bottom-right corner. No other brand marks.',
41
+ outputDir: 'public/assets/blog',
42
+ maxWords: 60,
43
+ realism: 'photorealistic',
44
+ },
45
+ },
46
+ ```
47
+
48
+ ## Wiring it into a site
49
+
50
+ ### 1. Configure `blogWriter` in `codejitsu.config.ts`
51
+
52
+ Fill in the block above. Take care with:
53
+ - `approvedTags` — exhaustive list. The commands will refuse to invent new tags.
54
+ - `imageStyle.description` — be specific. This is the seed for every image prompt.
55
+ - `seasonalRules` — free text. The batch generator reads this to balance topics by month.
56
+ - `bannedPhrases` — common AI tells.
57
+
58
+ ### 2. Run `codejitsu blog:init`
59
+
60
+ ```bash
61
+ npx codejitsu blog:init
62
+ ```
63
+
64
+ Copies the three slash command files into `.claude/commands/`:
65
+ - `blog.md`
66
+ - `blog-batch.md`
67
+ - `blog-images.md`
68
+
69
+ Each is a one-line reference pointing at the matching playbook in
70
+ `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/`. **Sites should
71
+ not edit these files.** Updates to the playbooks ship via `npm update`.
72
+
73
+ ### 3. Use in Claude Code
74
+
75
+ ```
76
+ /blog # single post, interactive
77
+ /blog "tankless water heater" # single post with topic seeded
78
+ /blog-batch 20 # schedule + outline 20 future posts
79
+ /blog-images # image prompts for next 10 pending posts
80
+ /blog-images 30 # for next 30
81
+ ```
82
+
83
+ ## How the slash commands work
84
+
85
+ Each `.claude/commands/blog.md` looks like:
86
+
87
+ ```md
88
+ ---
89
+ description: Codejitsu blog writer
90
+ argument-hint: [optional topic]
91
+ ---
92
+
93
+ Open `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_WRITING.md`
94
+ and follow it top-down. The playbook reads site-specific tone, services,
95
+ locations, and rules from `codejitsu.config.ts`.
96
+
97
+ Topic provided: $ARGUMENTS
98
+ ```
99
+
100
+ When the user runs `/blog`, Claude Code feeds this file's contents as the
101
+ prompt. Claude reads BLOG_WRITING.md from the installed package and follows
102
+ that playbook. **The actual writing rules live in the package**, not in the
103
+ site's repo — that's the robust part.
104
+
105
+ ## What must NOT be done
106
+
107
+ - **Don't copy the playbook contents into the site.** The slash command
108
+ should be a thin reference. If a site has hand-edited BLOG_WRITING.md
109
+ copy, it drifts from the package over time. Reference only.
110
+ - **Don't write posts that invent tags.** Only `approvedTags` are valid.
111
+ - **Don't break the 5-dash separator** for image prompts. Markdown's `---`
112
+ is the wrong separator — use `-----` literally.
113
+ - **Don't run `/blog-batch` with N > 50.** Too many posts in one batch
114
+ bunches topics and degrades quality. Three batches of 20 is better.
115
+ - **Don't generate prose for all N in `/blog-batch`.** Schedule + outlines
116
+ only. Prose per row via `/blog`.
117
+ - **Don't push or commit blog posts without user permission.**
118
+
119
+ ## Verify after setup
120
+
121
+ - [ ] `codejitsu.config.ts` has the full `blogWriter` block
122
+ - [ ] `.claude/commands/blog.md`, `blog-batch.md`, `blog-images.md` exist
123
+ - [ ] Each command is a one-line reference (don't edit)
124
+ - [ ] `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_WRITING.md` exists
125
+ - [ ] Running `/blog` in Claude Code opens the writer (test with a throwaway topic)
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Codejitsu blog batch — schedule + outline N future-dated posts
3
+ argument-hint: [N — number of posts, default 20]
4
+ ---
5
+
6
+ Open `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_BATCH.md` and follow it top-down.
7
+
8
+ The playbook generates a schedule + outline (NOT full prose) for the next N posts, reading services, locations, tags, cadence rules, and seasonal awareness from `codejitsu.config.ts`. Prose per row is filled in via `/blog`.
9
+
10
+ N provided: $ARGUMENTS
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Codejitsu blog image prompts — generate AI-image prompts for pending posts
3
+ argument-hint: [N — number of prompts, default 10]
4
+ ---
5
+
6
+ Open `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_IMAGES.md` and follow it top-down.
7
+
8
+ The playbook reads `blogWriter.imageStyle` from `codejitsu.config.ts` (style description, branding, output dir, max words, realism) and generates prompts in the **5-dash separator format** (not the markdown `---` HR — use `-----` literally).
9
+
10
+ Output goes to `Blog/IMAGE_PROMPTS.md`. Appends to existing file — never overwrites.
11
+
12
+ N provided: $ARGUMENTS
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Codejitsu blog writer — interactive single-post creation
3
+ argument-hint: [optional topic]
4
+ ---
5
+
6
+ Open `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_WRITING.md` and follow it top-down.
7
+
8
+ The playbook reads site-specific tone, services, locations, audience, approved tags, and writing rules from `codejitsu.config.ts` (the `blogWriter` block). Don't improvise from training-data memory — the config is the source of truth.
9
+
10
+ Topic provided: $ARGUMENTS
@@ -0,0 +1,59 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { c } from './format.mjs';
4
+
5
+ const PACKAGE_ROOT = path.resolve(
6
+ path.dirname(new URL(import.meta.url).pathname),
7
+ '..', '..', '..'
8
+ );
9
+
10
+ /**
11
+ * `codejitsu blog:init` — copies the blog-writer slash command templates
12
+ * into the site's `.claude/commands/` directory. The templates are thin
13
+ * references to playbooks that live in the package, so site updates flow
14
+ * via `npm update` without re-running this command.
15
+ */
16
+ export async function runBlogInit() {
17
+ const cwd = process.cwd();
18
+ const dest = path.join(cwd, '.claude/commands');
19
+ const src = path.join(
20
+ PACKAGE_ROOT,
21
+ 'modules/blog-writer/templates/.claude/commands'
22
+ );
23
+
24
+ if (!fs.existsSync(src)) {
25
+ console.error(c.red(`✗ Source dir missing: ${src}`));
26
+ console.error(' Reinstall @ibalzam/codejitsu-core.');
27
+ process.exit(1);
28
+ }
29
+
30
+ fs.mkdirSync(dest, { recursive: true });
31
+
32
+ const files = fs.readdirSync(src).filter((n) => n.endsWith('.md'));
33
+ console.log(c.bold('\nCodejitsu blog:init\n'));
34
+
35
+ let written = 0;
36
+ let skipped = 0;
37
+ for (const name of files) {
38
+ const destPath = path.join(dest, name);
39
+ if (fs.existsSync(destPath)) {
40
+ console.log(c.gray('= ') + `${name} (already exists, skipping)`);
41
+ skipped++;
42
+ continue;
43
+ }
44
+ fs.copyFileSync(path.join(src, name), destPath);
45
+ console.log(c.green('+ ') + `.claude/commands/${name}`);
46
+ written++;
47
+ }
48
+
49
+ console.log('');
50
+ console.log(`${written} created, ${skipped} skipped.`);
51
+ console.log('');
52
+ console.log('Next: configure the `blogWriter` block in codejitsu.config.ts.');
53
+ console.log('See `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/CLAUDE.md`.');
54
+ console.log('');
55
+ console.log('Then in Claude Code:');
56
+ console.log(' /blog single post, interactive');
57
+ console.log(' /blog-batch 20 schedule + outline 20 future posts');
58
+ console.log(' /blog-images image prompts for pending posts');
59
+ }
@@ -16,6 +16,55 @@ export interface CodejitsuConfig {
16
16
  deploy?: DeployConfig | false;
17
17
  contact?: ContactConfig | false;
18
18
  audit?: AuditConfig;
19
+ blogWriter?: BlogWriterConfig | false;
20
+ }
21
+ export interface BlogWriterConfig {
22
+ enabled?: boolean;
23
+ /** Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
24
+ tone: string;
25
+ /** What the company does + who it serves. Helps the writer ground posts in context. */
26
+ about: string;
27
+ /** Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
28
+ audience: string;
29
+ /** Service names that map to /services/<slug>/. Used for internal-link planning. */
30
+ services: string[];
31
+ /** Location names that map to /service-areas/<slug>/. */
32
+ locations: string[];
33
+ /** Exhaustive tag list. The writer refuses to invent new tags. */
34
+ approvedTags: string[];
35
+ wordCount: {
36
+ min: number;
37
+ max: number;
38
+ default: number;
39
+ };
40
+ faqs?: {
41
+ min: number;
42
+ max: number;
43
+ };
44
+ internalLinks?: {
45
+ min: number;
46
+ max: number;
47
+ };
48
+ /** Pricing policy. 'brackets-only' = always show as range with context. */
49
+ pricing?: 'brackets-only' | 'allowed' | 'never-mention';
50
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". */
51
+ seasonalRules?: string;
52
+ /** Phrases the writer must NOT produce. e.g. ["In today's fast-paced world", "Look no further"]. */
53
+ bannedPhrases?: string[];
54
+ /** Frontmatter `author` default if a post doesn't specify one. */
55
+ authorDefault?: string;
56
+ imageStyle: BlogImageStyle;
57
+ }
58
+ export interface BlogImageStyle {
59
+ /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
60
+ description: string;
61
+ /** Branding rule. e.g. "logo small in bottom-right corner, no other brand marks". */
62
+ branding: string;
63
+ /** Where final .webp files live. e.g. "public/assets/images/blog". */
64
+ outputDir: string;
65
+ /** Max words per generated prompt. Typical ≤ 60. */
66
+ maxWords: number;
67
+ realism: 'photorealistic' | 'illustration' | 'cartoon' | 'mixed';
19
68
  }
20
69
  export interface ContactConfig {
21
70
  enabled?: boolean;
@@ -17,6 +17,47 @@ export interface CodejitsuConfig {
17
17
  deploy?: DeployConfig | false;
18
18
  contact?: ContactConfig | false;
19
19
  audit?: AuditConfig;
20
+ blogWriter?: BlogWriterConfig | false;
21
+ }
22
+
23
+ export interface BlogWriterConfig {
24
+ enabled?: boolean;
25
+ /** Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
26
+ tone: string;
27
+ /** What the company does + who it serves. Helps the writer ground posts in context. */
28
+ about: string;
29
+ /** Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
30
+ audience: string;
31
+ /** Service names that map to /services/<slug>/. Used for internal-link planning. */
32
+ services: string[];
33
+ /** Location names that map to /service-areas/<slug>/. */
34
+ locations: string[];
35
+ /** Exhaustive tag list. The writer refuses to invent new tags. */
36
+ approvedTags: string[];
37
+ wordCount: { min: number; max: number; default: number };
38
+ faqs?: { min: number; max: number };
39
+ internalLinks?: { min: number; max: number };
40
+ /** Pricing policy. 'brackets-only' = always show as range with context. */
41
+ pricing?: 'brackets-only' | 'allowed' | 'never-mention';
42
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". */
43
+ seasonalRules?: string;
44
+ /** Phrases the writer must NOT produce. e.g. ["In today's fast-paced world", "Look no further"]. */
45
+ bannedPhrases?: string[];
46
+ /** Frontmatter `author` default if a post doesn't specify one. */
47
+ authorDefault?: string;
48
+ imageStyle: BlogImageStyle;
49
+ }
50
+
51
+ export interface BlogImageStyle {
52
+ /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
53
+ description: string;
54
+ /** Branding rule. e.g. "logo small in bottom-right corner, no other brand marks". */
55
+ branding: string;
56
+ /** Where final .webp files live. e.g. "public/assets/images/blog". */
57
+ outputDir: string;
58
+ /** Max words per generated prompt. Typical ≤ 60. */
59
+ maxWords: number;
60
+ realism: 'photorealistic' | 'illustration' | 'cartoon' | 'mixed';
20
61
  }
21
62
 
22
63
  export interface ContactConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibalzam/codejitsu-core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Shared core for Codejitsu Astro sites — reusable code and Claude-facing instructions for blog, SEO, images, deploy, and llms.txt.",
6
6
  "keywords": [