@ibalzam/codejitsu-core 0.11.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.
@@ -10,19 +10,28 @@
10
10
 
11
11
  ## Step 0 — Read site config
12
12
 
13
- Same as `BLOG_WRITING.md` Step 0: load `codejitsu.config.ts.blogWriter`.
13
+ Same as `BLOG_WRITING.md` Step 0: load `codejitsu.config.ts.blogWriter`. If
14
+ missing, STOP with the same error message.
14
15
 
15
16
  Also read:
16
- - `src/content/blog/` — existing posts (find the latest `pubDate`)
17
+ - `src/content/blog/` — existing posts (find latest `pubDate`)
17
18
  - `src/content.config.ts` — confirm the schema shape
18
19
 
19
20
  ## Step 1 — Decide cadence
20
21
 
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.
22
+ Default cadence is **`blogWriter.cadenceDays` from config** (kit default: 4).
24
23
 
25
- Starting date = max(`latest existing pubDate`, today) + cadence.
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.
26
35
 
27
36
  ## Step 2 — Generate N topics
28
37
 
@@ -8,21 +8,43 @@
8
8
  ## Step 0 — Read site config
9
9
 
10
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
+
11
32
  Everything in there is **site-specific input** for what you're about to write:
12
33
 
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`
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`
26
48
 
27
49
  Also detect the blog's frontmatter shape by reading `src/content.config.ts` and
28
50
  ONE existing post in `src/content/blog/`. Use that exact shape for the new post.
@@ -143,7 +165,12 @@ frontmatter shape exactly (detected in Step 0).
143
165
  - Phone: `[text](tel:+1...)` — use the actual number from `site.business.telephone`
144
166
  Best is one paragraph with both.
145
167
  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.
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.
147
174
  11. **Image placeholder.** Frontmatter `image` field points to where the image
148
175
  WILL live: `<imageStyle.outputDir>/<slug>.webp`. The file doesn't exist
149
176
  yet; that's fine. Run `/blog-images` to generate prompts later.
@@ -175,16 +202,22 @@ exactly. Examples:
175
202
 
176
203
  ## Step 4 — Verify after writing
177
204
 
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)
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.
188
221
 
189
222
  ## Step 5 — Report back
190
223
 
@@ -102,6 +102,27 @@ prompt. Claude reads BLOG_WRITING.md from the installed package and follows
102
102
  that playbook. **The actual writing rules live in the package**, not in the
103
103
  site's repo — that's the robust part.
104
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
+
105
126
  ## What must NOT be done
106
127
 
107
128
  - **Don't copy the playbook contents into the site.** The slash command
@@ -9,9 +9,9 @@ const PACKAGE_ROOT = path.resolve(
9
9
 
10
10
  /**
11
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.
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
15
  */
16
16
  export async function runBlogInit() {
17
17
  const cwd = process.cwd();
@@ -49,11 +49,43 @@ export async function runBlogInit() {
49
49
  console.log('');
50
50
  console.log(`${written} created, ${skipped} skipped.`);
51
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`.');
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
+
54
85
  console.log('');
55
86
  console.log('Then in Claude Code:');
56
87
  console.log(' /blog single post, interactive');
57
88
  console.log(' /blog-batch 20 schedule + outline 20 future posts');
58
89
  console.log(' /blog-images image prompts for pending posts');
90
+ console.log('');
59
91
  }
@@ -18,43 +18,87 @@ export interface CodejitsuConfig {
18
18
  audit?: AuditConfig;
19
19
  blogWriter?: BlogWriterConfig | false;
20
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
+ */
21
36
  export interface BlogWriterConfig {
22
37
  enabled?: boolean;
23
- /** Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
38
+ /** REQUIRED. Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
24
39
  tone: string;
25
- /** What the company does + who it serves. Helps the writer ground posts in context. */
40
+ /** REQUIRED. What the company does + who it serves. Grounds the writer in context. */
26
41
  about: string;
27
- /** Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
42
+ /** REQUIRED. Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
28
43
  audience: string;
29
- /** Service names that map to /services/<slug>/. Used for internal-link planning. */
44
+ /** REQUIRED. Service names mapping to /services/<slug>/. Used for internal-link planning. */
30
45
  services: string[];
31
- /** Location names that map to /service-areas/<slug>/. */
46
+ /** REQUIRED. Location names mapping to /service-areas/<slug>/. */
32
47
  locations: string[];
33
- /** Exhaustive tag list. The writer refuses to invent new tags. */
34
- approvedTags: string[];
35
- wordCount: {
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?: {
36
52
  min: number;
37
53
  max: number;
38
54
  default: number;
39
55
  };
56
+ /** Default: { min: 5, max: 8 }. */
40
57
  faqs?: {
41
58
  min: number;
42
59
  max: number;
43
60
  };
61
+ /** Default: { min: 3, max: 6 }. */
44
62
  internalLinks?: {
45
63
  min: number;
46
64
  max: number;
47
65
  };
48
- /** Pricing policy. 'brackets-only' = always show as range with context. */
66
+ /** Default: 'brackets-only' for service businesses; 'allowed' otherwise. */
49
67
  pricing?: 'brackets-only' | 'allowed' | 'never-mention';
50
- /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". */
68
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". Default: none. */
51
69
  seasonalRules?: string;
52
- /** Phrases the writer must NOT produce. e.g. ["In today's fast-paced world", "Look no further"]. */
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". */
53
71
  bannedPhrases?: string[];
54
- /** Frontmatter `author` default if a post doesn't specify one. */
72
+ /** Frontmatter `author` default. Default: `site.defaultAuthor` or `site.name`. */
55
73
  authorDefault?: string;
56
- imageStyle: BlogImageStyle;
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;
57
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
+ */
58
102
  export interface BlogImageStyle {
59
103
  /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
60
104
  description: string;
@@ -20,34 +20,78 @@ export interface CodejitsuConfig {
20
20
  blogWriter?: BlogWriterConfig | false;
21
21
  }
22
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
+ */
23
38
  export interface BlogWriterConfig {
24
39
  enabled?: boolean;
25
- /** Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
40
+ /** REQUIRED. Voice + register, free text. e.g. "professional but friendly, confident not boastful". */
26
41
  tone: string;
27
- /** What the company does + who it serves. Helps the writer ground posts in context. */
42
+ /** REQUIRED. What the company does + who it serves. Grounds the writer in context. */
28
43
  about: string;
29
- /** Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
44
+ /** REQUIRED. Primary reader. e.g. "BC Lower Mainland homeowners planning HVAC upgrades". */
30
45
  audience: string;
31
- /** Service names that map to /services/<slug>/. Used for internal-link planning. */
46
+ /** REQUIRED. Service names mapping to /services/<slug>/. Used for internal-link planning. */
32
47
  services: string[];
33
- /** Location names that map to /service-areas/<slug>/. */
48
+ /** REQUIRED. Location names mapping to /service-areas/<slug>/. */
34
49
  locations: string[];
35
- /** Exhaustive tag list. The writer refuses to invent new tags. */
36
- approvedTags: string[];
37
- wordCount: { min: number; max: number; default: number };
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 }. */
38
55
  faqs?: { min: number; max: number };
56
+ /** Default: { min: 3, max: 6 }. */
39
57
  internalLinks?: { min: number; max: number };
40
- /** Pricing policy. 'brackets-only' = always show as range with context. */
58
+ /** Default: 'brackets-only' for service businesses; 'allowed' otherwise. */
41
59
  pricing?: 'brackets-only' | 'allowed' | 'never-mention';
42
- /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". */
60
+ /** Free-text seasonal rules. e.g. "May-Sep: outdoor + AC; Oct-Nov: pre-winter prep". Default: none. */
43
61
  seasonalRules?: string;
44
- /** Phrases the writer must NOT produce. e.g. ["In today's fast-paced world", "Look no further"]. */
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". */
45
63
  bannedPhrases?: string[];
46
- /** Frontmatter `author` default if a post doesn't specify one. */
64
+ /** Frontmatter `author` default. Default: `site.defaultAuthor` or `site.name`. */
47
65
  authorDefault?: string;
48
- imageStyle: BlogImageStyle;
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;
49
70
  }
50
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
+ */
51
95
  export interface BlogImageStyle {
52
96
  /** Full prompt-style description of the visual style: palette, framing, materials, mood. */
53
97
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibalzam/codejitsu-core",
3
- "version": "0.11.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": [