@ibalzam/codejitsu-core 0.10.0 → 0.11.1

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,151 @@
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`. If
14
+ missing, STOP with the same error message.
15
+
16
+ Also read:
17
+ - `src/content/blog/` — existing posts (find latest `pubDate`)
18
+ - `src/content.config.ts` — confirm the schema shape
19
+
20
+ ## Step 1 — Decide cadence
21
+
22
+ Default cadence is **`blogWriter.cadenceDays` from config** (kit default: 4).
23
+
24
+ Cadence inference rules (in order):
25
+ 1. If `blogWriter.cadenceDays` is set in config → use it (don't infer).
26
+ 2. Else if `≥ 5` existing posts with future `pubDate` exist → measure gaps
27
+ between their consecutive dates. If the standard deviation of gaps is
28
+ `≤ 30%` of the median gap → use the median.
29
+ 3. Else (irregular spacing, e.g. WordPress import dates) → fall back to **4 days**.
30
+
31
+ Starting date = `max(latest existing pubDate, today) + cadence`.
32
+
33
+ If you fall back to default because of irregular existing dates, **mention it
34
+ to the user** before continuing — they may want to override.
35
+
36
+ ## Step 2 — Generate N topics
37
+
38
+ Brainstorm `N` topics, balancing across:
39
+
40
+ - **Service × location** combinations — cycle through `services` and
41
+ `locations` from config so coverage is even
42
+ - **Topic format** mix — roughly:
43
+ - 30% comparison / "X vs Y" posts
44
+ - 30% how-to / guides
45
+ - 20% troubleshooting
46
+ - 20% seasonal / news
47
+ - **Season awareness** — apply `seasonalRules` from config. A post landing
48
+ in July shouldn't be about "winterize"; one in December shouldn't be
49
+ about "pre-summer".
50
+ - **Unique titles** — no two posts in the batch should target the same slug
51
+ or near-duplicate keyword. Read all existing post titles before
52
+ brainstorming to avoid collisions.
53
+ - **Approved tag balance** — distribute tags so no single tag is over 40% of
54
+ the batch.
55
+
56
+ For each topic, derive:
57
+ - `slug` — kebab-case, includes the city when applicable
58
+ - `title` — natural, clickable, ≤70 chars
59
+ - `pubDate` — assigned by cadence walk
60
+ - `tags` — 2-3 from `approvedTags`, primary first
61
+ - `service` + `city` — for internal-link planning
62
+
63
+ ## Step 3 — Produce the schedule table
64
+
65
+ Write to `Blog/BLOG_SCHEDULE.md` (create the `Blog/` directory if needed).
66
+ Append to it if it already exists — never overwrite without asking.
67
+
68
+ Format:
69
+
70
+ ```markdown
71
+ # <Site Name> — Blog Schedule (<start month> - <end month> <year>)
72
+
73
+ Offline publishing calendar. N posts on a <cadence>-day cadence,
74
+ <start date> through <end date>.
75
+
76
+ ## How to use
77
+
78
+ 1. Pick a row. Slug = filename → `src/content/blog/<slug>.md`.
79
+ 2. `pubDate: <date>` in frontmatter controls visibility.
80
+ 3. Run `/blog "<title>"` to flesh out the post body.
81
+ 4. Run `/blog-images` once a batch of posts is drafted.
82
+
83
+ ## Schedule
84
+
85
+ | # | Date | Category | City | Slug | Title |
86
+ |---|------------|----------------|-----------|------|-------|
87
+ | 1 | YYYY-MM-DD | <primary tag> | <city> | `<slug>` | <Title> |
88
+ | 2 | ... | ... | ... | ... | ... |
89
+ ```
90
+
91
+ ## Step 4 — Produce per-post outlines
92
+
93
+ In the same file or a sibling `Blog/BLOG_OUTLINES.md`, expand each row into
94
+ an outline:
95
+
96
+ ```markdown
97
+ ## <Slug>
98
+
99
+ **pubDate**: YYYY-MM-DD
100
+ **target words**: <from config.wordCount.default>
101
+ **tags**: [primary, secondary]
102
+ **service**: <slug>
103
+ **city**: <slug or 'general'>
104
+
105
+ **Angle / hook**: <one-sentence framing of the reader's pain>
106
+
107
+ **Outline**:
108
+ - Intro — 1-2 paragraphs hooking <hook>
109
+ - ## <H2 #1>
110
+ - 2-3 paragraphs covering <points>
111
+ - ## <H2 #2>
112
+ - Comparison table of <X vs Y>
113
+ - ## <H2 #3>
114
+ - ...
115
+ - Closing CTA + 5-8 FAQs
116
+
117
+ **FAQ seeds**: list of 5-8 question stems
118
+ ```
119
+
120
+ ## Step 5 — Show + approve
121
+
122
+ Present the full schedule + outline file to the user. Ask:
123
+
124
+ > "Here's the schedule. Anything to adjust before we lock it in?
125
+ > Want me to flesh out the first N posts now, or stop here for review?"
126
+
127
+ If the user says go: run `/blog` for each row (one post at a time). If they
128
+ say stop: leave the file and exit. They can run `/blog` row by row whenever.
129
+
130
+ ## Hard rules
131
+
132
+ - **Don't write the full prose for all N in one shot.** Too long, too
133
+ expensive, hard to review. Schedule + outlines first; prose per row on
134
+ demand.
135
+ - **Don't push or commit.** The user reviews + commits.
136
+ - **Respect existing posts.** If `Blog/BLOG_SCHEDULE.md` already has rows,
137
+ append the new batch and don't renumber.
138
+ - **Don't invent services or locations.** Use only what's in
139
+ `blogWriter.services` and `blogWriter.locations`.
140
+ - **Don't invent tags.** Only from `approvedTags`.
141
+ - **Don't bunch up topic types.** Reject your own first draft if it has 5
142
+ comparison posts in a row.
143
+
144
+ ## Verify
145
+
146
+ - [ ] N rows, all unique slugs
147
+ - [ ] Dates respect cadence; no collisions with existing posts
148
+ - [ ] Tags balanced across `approvedTags`
149
+ - [ ] Service + city distribution covers most of the config lists
150
+ - [ ] Season fits each `pubDate`'s month
151
+ - [ ] 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,231 @@
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
+
12
+ **If `blogWriter` is missing or empty**, STOP and tell the user:
13
+
14
+ > "The blogWriter config block isn't set up yet. Add this to your codejitsu.config.ts:
15
+ >
16
+ > ```ts
17
+ > blogWriter: {
18
+ > tone: '<one-line voice description>',
19
+ > about: '<what the company does + who it serves>',
20
+ > audience: '<primary reader, e.g. BC homeowners planning HVAC>',
21
+ > services: ['Service A', 'Service B'],
22
+ > locations: ['City A', 'City B'],
23
+ > },
24
+ > ```
25
+ >
26
+ > The other fields (approvedTags, wordCount, imageStyle, pricing, seasonalRules,
27
+ > bannedPhrases) have sensible kit defaults. See `modules/blog-writer/CLAUDE.md`
28
+ > for the full shape."
29
+
30
+ Do not proceed without the block. Don't invent values.
31
+
32
+ Everything in there is **site-specific input** for what you're about to write:
33
+
34
+ - `tone` — voice + register (REQUIRED)
35
+ - `about` — what the company does, who it serves (REQUIRED)
36
+ - `audience` — primary reader (REQUIRED)
37
+ - `services` — names that map to `/services/<slug>/` (REQUIRED)
38
+ - `locations` — names that map to `/service-areas/<slug>/` (REQUIRED)
39
+ - `approvedTags` — exhaustive. If unset, derive from `blog.categories` in config OR ask the user once
40
+ - `wordCount` — `{ min, max, default }`. Kit default `{ 1200, 2500, 1800 }`
41
+ - `faqs` — `{ min, max }`. Kit default `{ 5, 8 }`
42
+ - `internalLinks` — `{ min, max }` per post. Kit default `{ 3, 6 }`
43
+ - `pricing` — `'brackets-only' | 'allowed' | 'never-mention'`. Kit default `'brackets-only'` for service businesses
44
+ - `seasonalRules` — free text; current date should respect this. Default: none
45
+ - `bannedPhrases` — refuse to write these. Kit default includes "In today's fast-paced world", "When it comes to", "Look no further", "In conclusion"
46
+ - `authorDefault` — frontmatter `author`. Default: `site.defaultAuthor` or `site.name`
47
+ - `cadenceDays` — for /blog-batch. Kit default `4`
48
+
49
+ Also detect the blog's frontmatter shape by reading `src/content.config.ts` and
50
+ ONE existing post in `src/content/blog/`. Use that exact shape for the new post.
51
+
52
+ ## Step 1 — Gather inputs (interactive)
53
+
54
+ Use `AskUserQuestion` to gather these in order. If the user passed a topic as
55
+ `$ARGUMENTS`, skip Question 1.
56
+
57
+ ### Question 1 — Topic & focus
58
+
59
+ ```
60
+ header: "Topic"
61
+ question: "What is this blog post about?"
62
+ options:
63
+ - "Industry guide" — Educational content the audience would search for
64
+ - "How-to / tutorial" — Step-by-step instructions
65
+ - "Comparison / vs" — Compare 2+ options on multiple attributes
66
+ - "Troubleshooting" — Diagnose-and-fix walkthrough
67
+ - "Seasonal / news" — Time-sensitive (rebate change, season prep, etc.)
68
+ ```
69
+
70
+ ### Question 2 — Length
71
+
72
+ ```
73
+ header: "Length"
74
+ question: "How long?"
75
+ options:
76
+ - "Short (400-600 words)" — Announcement, quick explainer
77
+ - "Medium (800-1200 words)" — Single-question explainer
78
+ - "Long (1500-2500 words)" — Default for SEO posts
79
+ - "XL (2500+ words)" — Definitive guide
80
+ ```
81
+
82
+ Default = the `wordCount.default` from config.
83
+
84
+ ### Question 3 — Primary service + city focus
85
+
86
+ ```
87
+ header: "Service + city"
88
+ question: "Which service and city is this anchored to?"
89
+ ```
90
+
91
+ Two follow-ups (separate questions):
92
+ - Service options: derive from `blogWriter.services`
93
+ - City options: derive from `blogWriter.locations` (plus an "any/general" option)
94
+
95
+ These drive the internal-link choices later.
96
+
97
+ ### Question 4 — Publish date
98
+
99
+ ```
100
+ header: "Publish date"
101
+ question: "When should this go live?"
102
+ options:
103
+ - "Today" — immediate publish
104
+ - "Next available slot" — read recent posts in src/content/blog/, find the
105
+ next open date in the existing cadence (e.g. 4-day gap), use that
106
+ - "Custom date" — user types YYYY-MM-DD
107
+ ```
108
+
109
+ ## Step 2 — Outline + approval
110
+
111
+ Before writing prose, produce an outline:
112
+
113
+ ```
114
+ TITLE: <working title>
115
+ SLUG: <kebab-case>
116
+ DATE: <YYYY-MM-DD>
117
+ TAGS: <2-3 from approvedTags, primary first>
118
+ WORDS: <target word count>
119
+
120
+ OUTLINE:
121
+ Intro (1-2 paragraphs)
122
+ ## <H2 #1>
123
+ 2-3 paragraphs of [angle]
124
+ ## <H2 #2>
125
+ Includes a comparison table / bullet list of [items]
126
+ ## <H2 #3>
127
+ ...
128
+ Closing CTA (modal + phone)
129
+ FAQs (5-8): planned questions
130
+ ```
131
+
132
+ Show the outline and **ask for approval before writing prose**. Adjust on
133
+ feedback. Don't proceed without approval.
134
+
135
+ ## Step 3 — Write the post
136
+
137
+ Write the full post to `src/content/blog/<slug>.md`. Match the existing
138
+ frontmatter shape exactly (detected in Step 0).
139
+
140
+ ### HARD RULES — apply to every post
141
+
142
+ 1. **No em dashes anywhere.** Use ` - ` (regular dash with spaces around it).
143
+ 2. **No H1 in markdown body.** Frontmatter `title` becomes the H1. Body starts
144
+ with intro paragraph(s), then `## H2`.
145
+ 3. **No "one-line H2 sections."** Every `## H2` must have at least
146
+ **2-3 paragraphs**, not just a sentence + bullet list.
147
+ 4. **At least one list per post.** Either a bullet list (3+ parallel items) or
148
+ a numbered list (if order matters) or a markdown table (for comparisons).
149
+ No exceptions — a wall-of-prose 2,000-word post is wrong. But also no
150
+ slide-deck (every section ending in bullets).
151
+ 5. **FAQ block** in frontmatter — `faqs.min` to `faqs.max` entries from config
152
+ (default 5-8). Each FAQ = real question a reader would search; answer is
153
+ 2-4 sentences, concise and complete.
154
+ 6. **Internal links** — `internalLinks.min` to `internalLinks.max` from config
155
+ (default 3-6). Link to `/services/<slug>/`, `/service-areas/<slug>/`, and
156
+ `/services/<slug>/<city>/` patterns. Don't link the same URL twice.
157
+ 7. **Banned phrases** — if `bannedPhrases` is set in config, refuse them.
158
+ Common offenders: "In today's fast-paced world…", "When it comes to…",
159
+ "Look no further…", "In conclusion…".
160
+ 8. **Pricing** — if `pricing: 'brackets-only'`, every price reference is a
161
+ range with context. Never a single dollar figure. If `pricing: 'never-mention'`,
162
+ omit pricing entirely.
163
+ 9. **CTA** — closing paragraph includes a call-to-action. Two patterns:
164
+ - Modal: `[text](#contact)` or trigger-class link
165
+ - Phone: `[text](tel:+1...)` — use the actual number from `site.business.telephone`
166
+ Best is one paragraph with both.
167
+ 10. **Approved tags only.** Pick 2-3 from `blogWriter.approvedTags`. Primary
168
+ tag first. **Never invent.** If nothing in `approvedTags` fits the topic,
169
+ STOP and ask the user:
170
+ > "None of your approved tags fit '<topic>'. Options: pick the closest fit
171
+ > from <list>, OR add a new tag to `blogWriter.approvedTags` in
172
+ > codejitsu.config.ts. Which?"
173
+ Don't auto-add tags to the config.
174
+ 11. **Image placeholder.** Frontmatter `image` field points to where the image
175
+ WILL live: `<imageStyle.outputDir>/<slug>.webp`. The file doesn't exist
176
+ yet; that's fine. Run `/blog-images` to generate prompts later.
177
+ 12. **Seasonal awareness.** If `seasonalRules` is set, the topic + angle must
178
+ fit the post's publish month. No "winterize" posts in July.
179
+
180
+ ### Structure
181
+
182
+ - **Intro** — 1-2 paragraphs that hook on a real audience pain. Tie to a
183
+ specific local concern when possible (the climate, regulation, market).
184
+ - **3-6 H2 sections** — each 2-3 paragraphs minimum. Mix prose, lists, and
185
+ tables. Roughly:
186
+ - Prose carries narrative + explanation in most sections
187
+ - 1-3 bullet lists where content is genuinely parallel items
188
+ - A markdown table for 2+ option comparisons (when it's a vs/comparison post)
189
+ - Numbered list only when order matters
190
+ - **H3 sub-sections** within H2s where useful.
191
+ - **Closing paragraph** with CTA.
192
+ - **FAQ block** in frontmatter.
193
+
194
+ ### Tone
195
+
196
+ Read `blogWriter.tone` and `blogWriter.audience` from config. Match
197
+ exactly. Examples:
198
+
199
+ - "professional but friendly, confident not boastful" → don't oversell, don't joke
200
+ - "BC HVAC pro, plain-spoken, technical when needed" → mix terms-of-art with plain explanations
201
+ - "Zen calm, helpful, occasional humour" → workzen's signature
202
+
203
+ ## Step 4 — Verify after writing
204
+
205
+ After writing the file, **actively run these checks** by reading the file back
206
+ and counting. Don't skip. If any fail, fix in place before reporting Step 5.
207
+
208
+ - Word count is within `wordCount` target (count after stripping frontmatter)
209
+ - FAQ count in frontmatter is within `faqs.min`-`faqs.max`
210
+ - Internal links count: grep for `](/services/` and `](/service-areas/` in body
211
+ - Tags are all from `approvedTags`
212
+ - **No em dashes**: grep body for `—` and `–` — neither should appear
213
+ - **No H1 in body**: first non-frontmatter line is NOT `# ...`
214
+ - **No `bannedPhrases`**: grep body for each banned phrase
215
+ - **Pricing policy**: if `'brackets-only'`, grep for standalone `$<digits>` not in a range
216
+ - **Frontmatter shape**: matches existing posts in `src/content/blog/`
217
+ - **At least one list**: body contains `- `, `1. `, OR `|` (markdown table)
218
+
219
+ If any check fails, **fix the file then re-verify**. Surface unfixable issues
220
+ to the user before reporting Step 5.
221
+
222
+ ## Step 5 — Report back
223
+
224
+ Tell the user:
225
+ - Path to the new file
226
+ - Word count
227
+ - Tag selection + why
228
+ - Whether image needs generating (point at `/blog-images`)
229
+ - Whether the post is published immediately or scheduled (based on date)
230
+
231
+ Don't push or commit unless explicitly asked.
@@ -0,0 +1,146 @@
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
+ ## Conflict with existing site blog instructions
106
+
107
+ Some Codejitsu sites already have `.claude/BLOG_INSTRUCTIONS.md` (pearl,
108
+ workzen) — older bespoke instructions written before the kit existed.
109
+ After running `blog:init`, the site has BOTH:
110
+ - The kit's `/blog` command → reads BLOG_WRITING.md in the package
111
+ - The site's `.claude/BLOG_INSTRUCTIONS.md` → may be referenced by Claude
112
+ ambiently when the site is open
113
+
114
+ **Resolution when adopting the kit on such a site:**
115
+ 1. Read the existing `.claude/BLOG_INSTRUCTIONS.md`. Identify what's
116
+ genuinely site-specific (specific tone phrasings, special pricing
117
+ rules, internal link patterns).
118
+ 2. Move those site-specific bits into `codejitsu.config.ts.blogWriter`
119
+ (or `seasonalRules` / `bannedPhrases` fields).
120
+ 3. Delete or rename the old `.claude/BLOG_INSTRUCTIONS.md` to
121
+ `.claude/BLOG_INSTRUCTIONS.archived.md` so it doesn't get auto-loaded.
122
+ 4. Keep only the kit's slash command files in `.claude/commands/`.
123
+
124
+ This avoids the "two contradicting instruction sources" problem.
125
+
126
+ ## What must NOT be done
127
+
128
+ - **Don't copy the playbook contents into the site.** The slash command
129
+ should be a thin reference. If a site has hand-edited BLOG_WRITING.md
130
+ copy, it drifts from the package over time. Reference only.
131
+ - **Don't write posts that invent tags.** Only `approvedTags` are valid.
132
+ - **Don't break the 5-dash separator** for image prompts. Markdown's `---`
133
+ is the wrong separator — use `-----` literally.
134
+ - **Don't run `/blog-batch` with N > 50.** Too many posts in one batch
135
+ bunches topics and degrades quality. Three batches of 20 is better.
136
+ - **Don't generate prose for all N in `/blog-batch`.** Schedule + outlines
137
+ only. Prose per row via `/blog`.
138
+ - **Don't push or commit blog posts without user permission.**
139
+
140
+ ## Verify after setup
141
+
142
+ - [ ] `codejitsu.config.ts` has the full `blogWriter` block
143
+ - [ ] `.claude/commands/blog.md`, `blog-batch.md`, `blog-images.md` exist
144
+ - [ ] Each command is a one-line reference (don't edit)
145
+ - [ ] `node_modules/@ibalzam/codejitsu-core/modules/blog-writer/BLOG_WRITING.md` exists
146
+ - [ ] 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,91 @@
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/`. The templates are thin references
13
+ * to playbooks that live in the package; updates flow via `npm update`
14
+ * 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
+
53
+ // Detect whether blogWriter is already in the config.
54
+ const configCandidates = ['codejitsu.config.ts', 'codejitsu.config.mjs', 'codejitsu.config.json'];
55
+ let hasBlogWriter = false;
56
+ for (const name of configCandidates) {
57
+ const p = path.join(cwd, name);
58
+ if (fs.existsSync(p) && /blogWriter\s*:/.test(fs.readFileSync(p, 'utf8'))) {
59
+ hasBlogWriter = true;
60
+ break;
61
+ }
62
+ }
63
+
64
+ if (hasBlogWriter) {
65
+ console.log(c.green('✓') + ' `blogWriter` block detected in codejitsu.config — ready to use.');
66
+ } else {
67
+ console.log(c.yellow('!') + ' No `blogWriter` block in codejitsu.config yet.');
68
+ console.log('');
69
+ console.log(c.gray('Add this minimal block to your codejitsu.config.ts:'));
70
+ console.log('');
71
+ console.log(c.gray(' blogWriter: {'));
72
+ console.log(c.gray(" tone: 'professional, plain-spoken, confident not boastful',"));
73
+ console.log(c.gray(" about: '<what the company does + who it serves>',"));
74
+ console.log(c.gray(" audience: '<primary reader, e.g. BC homeowners ...>',"));
75
+ console.log(c.gray(" services: ['Service A', 'Service B'],"));
76
+ console.log(c.gray(" locations: ['City A', 'City B'],"));
77
+ console.log(c.gray(' // Optional with kit defaults: approvedTags, wordCount, faqs,'));
78
+ console.log(c.gray(' // internalLinks, pricing, seasonalRules, bannedPhrases,'));
79
+ console.log(c.gray(' // authorDefault, cadenceDays, imageStyle'));
80
+ console.log(c.gray(' },'));
81
+ console.log('');
82
+ console.log(c.gray('See node_modules/@ibalzam/codejitsu-core/modules/blog-writer/CLAUDE.md for the full shape.'));
83
+ }
84
+
85
+ console.log('');
86
+ console.log('Then in Claude Code:');
87
+ console.log(' /blog single post, interactive');
88
+ console.log(' /blog-batch 20 schedule + outline 20 future posts');
89
+ console.log(' /blog-images image prompts for pending posts');
90
+ console.log('');
91
+ }
@@ -16,6 +16,99 @@ export interface CodejitsuConfig {
16
16
  deploy?: DeployConfig | false;
17
17
  contact?: ContactConfig | false;
18
18
  audit?: AuditConfig;
19
+ blogWriter?: BlogWriterConfig | false;
20
+ }
21
+ /**
22
+ * The minimum to enable blog writing: tone, about, audience, services,
23
+ * locations. Everything else has kit defaults that suit most sites.
24
+ *
25
+ * @example minimal
26
+ * ```ts
27
+ * blogWriter: {
28
+ * tone: 'professional, plain-spoken, confident not boastful',
29
+ * about: 'Veteran is a BC HVAC contractor serving the Lower Mainland',
30
+ * audience: 'BC homeowners planning HVAC upgrades',
31
+ * services: ['Heat Pump Installation', 'Furnace Installation'],
32
+ * locations: ['Vancouver', 'Burnaby', 'Surrey'],
33
+ * }
34
+ * ```
35
+ */
36
+ export interface BlogWriterConfig {
37
+ enabled?: boolean;
38
+ /** REQUIRED. Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
39
+ tone: string;
40
+ /** REQUIRED. What the company does + who it serves. Grounds the writer in context. */
41
+ about: string;
42
+ /** REQUIRED. Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
43
+ audience: string;
44
+ /** REQUIRED. Service names mapping to /services/<slug>/. Used for internal-link planning. */
45
+ services: string[];
46
+ /** REQUIRED. Location names mapping to /service-areas/<slug>/. */
47
+ locations: string[];
48
+ /** Exhaustive tag list. The writer refuses to invent new tags; if nothing fits it asks the user. Default: derives from `blog.categories` in config, or asks at first /blog use. */
49
+ approvedTags?: string[];
50
+ /** Default: { min: 1200, max: 2500, default: 1800 } (the "Long" tier). */
51
+ wordCount?: {
52
+ min: number;
53
+ max: number;
54
+ default: number;
55
+ };
56
+ /** Default: { min: 5, max: 8 }. */
57
+ faqs?: {
58
+ min: number;
59
+ max: number;
60
+ };
61
+ /** Default: { min: 3, max: 6 }. */
62
+ internalLinks?: {
63
+ min: number;
64
+ max: number;
65
+ };
66
+ /** Default: 'brackets-only' for service businesses; 'allowed' otherwise. */
67
+ pricing?: 'brackets-only' | 'allowed' | 'never-mention';
68
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". Default: none. */
69
+ seasonalRules?: string;
70
+ /** Phrases the writer must NOT produce. Default kit list includes "In today's fast-paced world", "When it comes to", "Look no further", "In conclusion". */
71
+ bannedPhrases?: string[];
72
+ /** Frontmatter `author` default. Default: `site.defaultAuthor` or `site.name`. */
73
+ authorDefault?: string;
74
+ /** Cadence for /blog-batch, in days. Default: 4. */
75
+ cadenceDays?: number;
76
+ /** Image generation config. Required only if you'll use /blog-images. */
77
+ imageStyle?: BlogImageStyle;
78
+ }
79
+ /**
80
+ * @example photorealistic-architecture
81
+ * ```ts
82
+ * imageStyle: {
83
+ * description: 'Photorealistic real-estate / architectural photography of the actual space the post is about. Wide landscape framing, eye-level. Bright natural daylight, modern desert-contemporary palette. Clean composition, lightly staged, no clutter, no text overlays.',
84
+ * branding: 'Logo small in bottom-right corner. No other brand marks.',
85
+ * outputDir: 'public/assets/images/blog',
86
+ * maxWords: 60,
87
+ * realism: 'photorealistic',
88
+ * }
89
+ * ```
90
+ *
91
+ * @example sloth-mascot-cartoon
92
+ * ```ts
93
+ * imageStyle: {
94
+ * description: 'Cartoon sloth-mascot character relevant to the post topic, flat friendly colors, with a white rounded text box at the bottom containing a SHORT version of the title.',
95
+ * branding: 'No watermarks, just the in-image character',
96
+ * outputDir: 'public/images/blog/posts',
97
+ * maxWords: 50,
98
+ * realism: 'cartoon',
99
+ * }
100
+ * ```
101
+ */
102
+ export interface BlogImageStyle {
103
+ /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
104
+ description: string;
105
+ /** Branding rule. e.g. "logo small in bottom-right corner, no other brand marks". */
106
+ branding: string;
107
+ /** Where final .webp files live. e.g. "public/assets/images/blog". */
108
+ outputDir: string;
109
+ /** Max words per generated prompt. Typical ≤ 60. */
110
+ maxWords: number;
111
+ realism: 'photorealistic' | 'illustration' | 'cartoon' | 'mixed';
19
112
  }
20
113
  export interface ContactConfig {
21
114
  enabled?: boolean;
@@ -17,6 +17,91 @@ export interface CodejitsuConfig {
17
17
  deploy?: DeployConfig | false;
18
18
  contact?: ContactConfig | false;
19
19
  audit?: AuditConfig;
20
+ blogWriter?: BlogWriterConfig | false;
21
+ }
22
+
23
+ /**
24
+ * The minimum to enable blog writing: tone, about, audience, services,
25
+ * locations. Everything else has kit defaults that suit most sites.
26
+ *
27
+ * @example minimal
28
+ * ```ts
29
+ * blogWriter: {
30
+ * tone: 'professional, plain-spoken, confident not boastful',
31
+ * about: 'Veteran is a BC HVAC contractor serving the Lower Mainland',
32
+ * audience: 'BC homeowners planning HVAC upgrades',
33
+ * services: ['Heat Pump Installation', 'Furnace Installation'],
34
+ * locations: ['Vancouver', 'Burnaby', 'Surrey'],
35
+ * }
36
+ * ```
37
+ */
38
+ export interface BlogWriterConfig {
39
+ enabled?: boolean;
40
+ /** REQUIRED. Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
41
+ tone: string;
42
+ /** REQUIRED. What the company does + who it serves. Grounds the writer in context. */
43
+ about: string;
44
+ /** REQUIRED. Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
45
+ audience: string;
46
+ /** REQUIRED. Service names mapping to /services/<slug>/. Used for internal-link planning. */
47
+ services: string[];
48
+ /** REQUIRED. Location names mapping to /service-areas/<slug>/. */
49
+ locations: string[];
50
+ /** Exhaustive tag list. The writer refuses to invent new tags; if nothing fits it asks the user. Default: derives from `blog.categories` in config, or asks at first /blog use. */
51
+ approvedTags?: string[];
52
+ /** Default: { min: 1200, max: 2500, default: 1800 } (the "Long" tier). */
53
+ wordCount?: { min: number; max: number; default: number };
54
+ /** Default: { min: 5, max: 8 }. */
55
+ faqs?: { min: number; max: number };
56
+ /** Default: { min: 3, max: 6 }. */
57
+ internalLinks?: { min: number; max: number };
58
+ /** Default: 'brackets-only' for service businesses; 'allowed' otherwise. */
59
+ pricing?: 'brackets-only' | 'allowed' | 'never-mention';
60
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". Default: none. */
61
+ seasonalRules?: string;
62
+ /** Phrases the writer must NOT produce. Default kit list includes "In today's fast-paced world", "When it comes to", "Look no further", "In conclusion". */
63
+ bannedPhrases?: string[];
64
+ /** Frontmatter `author` default. Default: `site.defaultAuthor` or `site.name`. */
65
+ authorDefault?: string;
66
+ /** Cadence for /blog-batch, in days. Default: 4. */
67
+ cadenceDays?: number;
68
+ /** Image generation config. Required only if you'll use /blog-images. */
69
+ imageStyle?: BlogImageStyle;
70
+ }
71
+
72
+ /**
73
+ * @example photorealistic-architecture
74
+ * ```ts
75
+ * imageStyle: {
76
+ * description: 'Photorealistic real-estate / architectural photography of the actual space the post is about. Wide landscape framing, eye-level. Bright natural daylight, modern desert-contemporary palette. Clean composition, lightly staged, no clutter, no text overlays.',
77
+ * branding: 'Logo small in bottom-right corner. No other brand marks.',
78
+ * outputDir: 'public/assets/images/blog',
79
+ * maxWords: 60,
80
+ * realism: 'photorealistic',
81
+ * }
82
+ * ```
83
+ *
84
+ * @example sloth-mascot-cartoon
85
+ * ```ts
86
+ * imageStyle: {
87
+ * description: 'Cartoon sloth-mascot character relevant to the post topic, flat friendly colors, with a white rounded text box at the bottom containing a SHORT version of the title.',
88
+ * branding: 'No watermarks, just the in-image character',
89
+ * outputDir: 'public/images/blog/posts',
90
+ * maxWords: 50,
91
+ * realism: 'cartoon',
92
+ * }
93
+ * ```
94
+ */
95
+ export interface BlogImageStyle {
96
+ /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
97
+ description: string;
98
+ /** Branding rule. e.g. "logo small in bottom-right corner, no other brand marks". */
99
+ branding: string;
100
+ /** Where final .webp files live. e.g. "public/assets/images/blog". */
101
+ outputDir: string;
102
+ /** Max words per generated prompt. Typical ≤ 60. */
103
+ maxWords: number;
104
+ realism: 'photorealistic' | 'illustration' | 'cartoon' | 'mixed';
20
105
  }
21
106
 
22
107
  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.1",
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": [