@ibalzam/codejitsu-core 0.1.0 → 0.2.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.
@@ -1,201 +1,5 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import matter from 'gray-matter';
4
- import readingTime from 'reading-time';
5
-
6
- export interface FAQItem {
7
- question: string;
8
- answer: string;
9
- }
10
-
11
- export interface BlogPostFrontmatter {
12
- title: string;
13
- description: string;
14
- date: string;
15
- slug?: string;
16
- author?: string;
17
- image?: string;
18
- tags?: string[];
19
- faqs?: FAQItem[];
20
- }
21
-
22
- export interface BlogPostMetadata {
23
- slug: string;
24
- title: string;
25
- description: string;
26
- date: string;
27
- author?: string;
28
- image?: string;
29
- tags?: string[];
30
- readingTime: string;
31
- }
32
-
33
- export interface BlogPost extends BlogPostMetadata {
34
- faqs?: FAQItem[];
35
- content: string;
36
- }
37
-
38
- export interface BlogCategory {
39
- slug: string;
40
- tag: string;
41
- title: string;
42
- subtitle: string;
43
- metaDescription: string;
44
- }
45
-
46
- export interface BlogConfig {
47
- contentDir?: string;
48
- defaultAuthor?: string;
49
- categories?: BlogCategory[];
50
- }
51
-
52
- export interface BlogAPI {
53
- getAllPosts(): Promise<BlogPostMetadata[]>;
54
- getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]>;
55
- getFutureBlogSlugs(): Promise<string[]>;
56
- getAllPostSlugs(): Promise<string[]>;
57
- getPostBySlug(slug: string): Promise<BlogPost | null>;
58
- getAllTags(): Promise<string[]>;
59
- getPostsByTag(tag: string): Promise<BlogPostMetadata[]>;
60
- getAllCategorySlugs(): string[];
61
- getCategoryBySlug(slug: string): BlogCategory | undefined;
62
- getPostsByCategory(slug: string): Promise<BlogPostMetadata[]>;
63
- categories: BlogCategory[];
64
- }
65
-
66
- function getTodayUTC(): Date {
67
- const now = new Date();
68
- return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
69
- }
70
-
71
- function fileSlug(name: string): string {
72
- return name.replace(/\.md$/, '');
73
- }
74
-
75
- function canonicalSlugFor(name: string, fm: { slug?: string }): string {
76
- return fm.slug || fileSlug(name);
77
- }
78
-
79
- export function createBlog(config: BlogConfig = {}): BlogAPI {
80
- const contentDir = path.resolve(process.cwd(), config.contentDir ?? 'content/blog');
81
- const defaultAuthor = config.defaultAuthor;
82
- const categories = config.categories ?? [];
83
-
84
- function readAllFiles() {
85
- if (!fs.existsSync(contentDir)) return [];
86
- return fs.readdirSync(contentDir)
87
- .filter((n) => n.endsWith('.md'))
88
- .map((fileName) => {
89
- const raw = fs.readFileSync(path.join(contentDir, fileName), 'utf8');
90
- const parsed = matter(raw);
91
- const data = parsed.data as Partial<BlogPostFrontmatter>;
92
- return {
93
- fileName,
94
- fileSlug: fileSlug(fileName),
95
- canonicalSlug: canonicalSlugFor(fileName, data),
96
- data,
97
- content: parsed.content,
98
- };
99
- });
100
- }
101
-
102
- function toMetadata(f: ReturnType<typeof readAllFiles>[number]): BlogPostMetadata {
103
- return {
104
- slug: f.canonicalSlug,
105
- title: f.data.title ?? '',
106
- description: f.data.description ?? '',
107
- date: f.data.date ?? '',
108
- author: f.data.author ?? defaultAuthor,
109
- image: f.data.image,
110
- tags: f.data.tags,
111
- readingTime: readingTime(f.content).text,
112
- };
113
- }
114
-
115
- async function getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]> {
116
- return readAllFiles()
117
- .map(toMetadata)
118
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
119
- }
120
-
121
- async function getAllPosts(): Promise<BlogPostMetadata[]> {
122
- const today = getTodayUTC();
123
- const all = await getAllPostsIncludingFuture();
124
- return all.filter((p) => new Date(p.date) <= today);
125
- }
126
-
127
- async function getFutureBlogSlugs(): Promise<string[]> {
128
- const today = getTodayUTC();
129
- const slugs = new Set<string>();
130
- for (const f of readAllFiles()) {
131
- if (!f.data.date) continue;
132
- if (new Date(f.data.date) > today) {
133
- slugs.add(f.canonicalSlug);
134
- if (f.fileSlug !== f.canonicalSlug) slugs.add(f.fileSlug);
135
- }
136
- }
137
- return Array.from(slugs);
138
- }
139
-
140
- async function getAllPostSlugs(): Promise<string[]> {
141
- const slugs = new Set<string>();
142
- for (const f of readAllFiles()) {
143
- slugs.add(f.fileSlug);
144
- if (f.canonicalSlug !== f.fileSlug) slugs.add(f.canonicalSlug);
145
- }
146
- return Array.from(slugs);
147
- }
148
-
149
- async function getPostBySlug(slug: string): Promise<BlogPost | null> {
150
- const match = readAllFiles().find(
151
- (f) => f.canonicalSlug === slug || f.fileSlug === slug
152
- );
153
- if (!match) return null;
154
- return {
155
- ...toMetadata(match),
156
- faqs: match.data.faqs,
157
- content: match.content,
158
- };
159
- }
160
-
161
- async function getAllTags(): Promise<string[]> {
162
- const posts = await getAllPosts();
163
- const tags = new Set<string>();
164
- posts.forEach((p) => p.tags?.forEach((t) => tags.add(t)));
165
- return Array.from(tags).sort();
166
- }
167
-
168
- async function getPostsByTag(tag: string): Promise<BlogPostMetadata[]> {
169
- const posts = await getAllPosts();
170
- return posts.filter((p) => p.tags?.includes(tag));
171
- }
172
-
173
- function getAllCategorySlugs(): string[] {
174
- return categories.map((c) => c.slug);
175
- }
176
-
177
- function getCategoryBySlug(slug: string): BlogCategory | undefined {
178
- return categories.find((c) => c.slug === slug);
179
- }
180
-
181
- async function getPostsByCategory(slug: string): Promise<BlogPostMetadata[]> {
182
- const cat = getCategoryBySlug(slug);
183
- if (!cat) return [];
184
- const posts = await getAllPosts();
185
- return posts.filter((p) => p.tags?.includes(cat.tag));
186
- }
187
-
188
- return {
189
- getAllPosts,
190
- getAllPostsIncludingFuture,
191
- getFutureBlogSlugs,
192
- getAllPostSlugs,
193
- getPostBySlug,
194
- getAllTags,
195
- getPostsByTag,
196
- getAllCategorySlugs,
197
- getCategoryBySlug,
198
- getPostsByCategory,
199
- categories,
200
- };
201
- }
1
+ export * from './types.js';
2
+ export { createBlog } from './fs.js';
3
+ export type { FsBlogConfig } from './fs.js';
4
+ export { createBlogFromCollection } from './collection.js';
5
+ export type { CollectionBlogConfig } from './collection.js';
@@ -0,0 +1,71 @@
1
+ export interface FAQItem {
2
+ question: string;
3
+ answer: string;
4
+ }
5
+
6
+ export interface BlogPostFrontmatter {
7
+ title: string;
8
+ description: string;
9
+ date: string | Date;
10
+ slug?: string;
11
+ author?: string;
12
+ image?: string;
13
+ tags?: string[];
14
+ faqs?: FAQItem[];
15
+ draft?: boolean;
16
+ /** Additional fields. */
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export interface BlogPostMetadata {
21
+ slug: string;
22
+ title: string;
23
+ description: string;
24
+ /** ISO date string (YYYY-MM-DD or full ISO). */
25
+ date: string;
26
+ author?: string;
27
+ image?: string;
28
+ tags?: string[];
29
+ readingTime: string;
30
+ }
31
+
32
+ export interface BlogPost extends BlogPostMetadata {
33
+ faqs?: FAQItem[];
34
+ /** Raw markdown body (fs mode) or rendered HTML (CC mode). See per-function notes. */
35
+ content: string;
36
+ }
37
+
38
+ export interface BlogCategory {
39
+ slug: string;
40
+ tag: string;
41
+ title: string;
42
+ subtitle: string;
43
+ metaDescription: string;
44
+ }
45
+
46
+ export interface BlogAPI {
47
+ /** Published posts only (date <= today, not draft). Sorted newest first. */
48
+ getAllPosts(): Promise<BlogPostMetadata[]>;
49
+ /** All non-draft posts including future-dated ones. Sorted newest first. */
50
+ getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]>;
51
+ /** Slugs of non-draft posts with a future date. */
52
+ getFutureBlogSlugs(): Promise<string[]>;
53
+ /** Every slug needed for static path generation (includes future-dated, excludes drafts). */
54
+ getAllPostSlugs(): Promise<string[]>;
55
+ getPostBySlug(slug: string): Promise<BlogPost | null>;
56
+ getAllTags(): Promise<string[]>;
57
+ getPostsByTag(tag: string): Promise<BlogPostMetadata[]>;
58
+ getAllCategorySlugs(): string[];
59
+ getCategoryBySlug(slug: string): BlogCategory | undefined;
60
+ getPostsByCategory(slug: string): Promise<BlogPostMetadata[]>;
61
+ categories: BlogCategory[];
62
+ }
63
+
64
+ export interface CommonBlogConfig {
65
+ defaultAuthor?: string;
66
+ categories?: BlogCategory[];
67
+ /** Frontmatter field name for the date. Default 'date'. */
68
+ dateField?: string;
69
+ /** Frontmatter field name for the draft flag. Default null (no draft support). */
70
+ draftField?: string | null;
71
+ }
@@ -0,0 +1,27 @@
1
+ // Drop into `src/content.config.ts` (Astro). Adjust schema to your needs.
2
+ import { defineCollection, z } from 'astro:content';
3
+ import { glob } from 'astro/loaders';
4
+
5
+ const blog = defineCollection({
6
+ loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
7
+ schema: z.object({
8
+ title: z.string(),
9
+ description: z.string(),
10
+ pubDate: z.coerce.date(),
11
+ updatedDate: z.coerce.date().optional(),
12
+ author: z.string().default('editor'),
13
+ image: z.string().optional(),
14
+ tags: z.array(z.string()).default([]),
15
+ draft: z.boolean().default(false),
16
+ faqs: z
17
+ .array(
18
+ z.object({
19
+ question: z.string(),
20
+ answer: z.string(),
21
+ })
22
+ )
23
+ .optional(),
24
+ }),
25
+ });
26
+
27
+ export const collections = { blog };
@@ -0,0 +1,14 @@
1
+ // FS + gray-matter variant — use for non-Astro projects (Next.js, etc.).
2
+ // For Astro, see `blog.ts` (uses Content Collections).
3
+
4
+ import { createBlog, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
5
+
6
+ const categories: BlogCategory[] = [];
7
+
8
+ export const blog = createBlog({
9
+ contentDir: 'content/blog',
10
+ dateField: 'date', // 'date' or 'pubDate' depending on your frontmatter
11
+ draftField: null, // set to 'draft' if your frontmatter uses it
12
+ defaultAuthor: 'TODO: Site Author',
13
+ categories,
14
+ });
@@ -1,4 +1,7 @@
1
- import { createBlog, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
1
+ // Astro Content Collections variant recommended for Astro sites.
2
+ // For non-Astro projects, see `blog-fs.ts` (uses gray-matter directly).
3
+
4
+ import { createBlogFromCollection, type BlogCategory } from '@ibalzam/codejitsu-core/blog';
2
5
 
3
6
  const categories: BlogCategory[] = [
4
7
  // {
@@ -10,8 +13,13 @@ const categories: BlogCategory[] = [
10
13
  // },
11
14
  ];
12
15
 
13
- export const blog = createBlog({
14
- contentDir: 'content/blog',
16
+ export const blog = createBlogFromCollection({
17
+ collectionName: 'blog',
18
+ // Match the field name from your Astro CC schema (`src/content.config.ts`).
19
+ // Common choices: 'date' (default) or 'pubDate'.
20
+ dateField: 'pubDate',
21
+ // Set to null if your schema has no `draft` field.
22
+ draftField: 'draft',
15
23
  defaultAuthor: 'TODO: Site Author',
16
24
  categories,
17
25
  });
@@ -0,0 +1,121 @@
1
+ # Config module — instructions for Claude
2
+
3
+ The unified config drives every other module. When you set up a Codejitsu site, **always create `codejitsu.config.ts` first** — every other module reads from it.
4
+
5
+ ## What this module provides
6
+
7
+ - `defineConfig(config)` — identity helper that types your config (autocomplete + validation in editors).
8
+ - `loadConfig(cwd?)` — used internally by the CLIs to resolve the config from the site's working directory.
9
+ - `isModuleEnabled(config, name)` — utility for module-aware code paths.
10
+ - Full type definitions (`CodejitsuConfig`, `SiteConfig`, `BlogConfig`, etc.).
11
+
12
+ ## Wiring into a site
13
+
14
+ ### 1. Create `codejitsu.config.ts` at the site root
15
+
16
+ ```ts
17
+ import { defineConfig } from '@ibalzam/codejitsu-core/config';
18
+
19
+ export default defineConfig({
20
+ site: {
21
+ url: 'https://example.com',
22
+ name: 'Example',
23
+ titleSuffix: ' — Example',
24
+ defaultAuthor: 'editor',
25
+ defaultOgImage: '/og-image.webp',
26
+ locale: 'en-US',
27
+ business: {
28
+ telephone: '+1-555-555-5555',
29
+ email: 'hello@example.com',
30
+ address: { addressLocality: 'Las Vegas', addressRegion: 'NV', addressCountry: 'US' },
31
+ areaServed: ['Las Vegas', 'Henderson'],
32
+ },
33
+ },
34
+
35
+ blog: {
36
+ mode: 'collection', // Astro Content Collections
37
+ collectionName: 'blog',
38
+ dateField: 'pubDate', // pearl pattern
39
+ draftField: 'draft',
40
+ },
41
+
42
+ seo: {
43
+ sitemap: {
44
+ excludePatterns: [/\/lp\//],
45
+ },
46
+ defaultSchemas: ['localBusiness', 'website'],
47
+ },
48
+
49
+ images: {
50
+ sourceDir: 'public/assets/images',
51
+ defaultQuality: 82,
52
+ defaultMaxSize: 1376,
53
+ specialRules: {
54
+ 'logos/logo': { maxWidth: 400, quality: 35 },
55
+ },
56
+ },
57
+
58
+ llms: {
59
+ mode: 'content-scan',
60
+ contentScan: {
61
+ servicesDir: 'src/content/services',
62
+ locationsDir: 'src/content/locations',
63
+ pagesDir: 'src/pages',
64
+ },
65
+ blogDir: 'src/content/blog',
66
+ },
67
+
68
+ deploy: { cloudflarePagesName: 'example' },
69
+ });
70
+ ```
71
+
72
+ ### 2. Install jiti (only if using `.ts`)
73
+
74
+ ```bash
75
+ npm install -D jiti
76
+ ```
77
+
78
+ `.mjs` and `.json` configs work without jiti.
79
+
80
+ ### 3. The CLIs find it automatically
81
+
82
+ `codejitsu-optimize-images`, `codejitsu-llms`, and `codejitsu-check` all read from this one file. No more per-module config files.
83
+
84
+ ## Search order
85
+
86
+ The loader looks for, in order:
87
+ 1. `codejitsu.config.ts`
88
+ 2. `codejitsu.config.mts`
89
+ 3. `codejitsu.config.mjs`
90
+ 4. `codejitsu.config.js`
91
+ 5. `codejitsu.config.json`
92
+ 6. `codejitsu` key in `package.json`
93
+
94
+ First match wins. Stop searching after one is found.
95
+
96
+ ## Disabling a module
97
+
98
+ Two ways:
99
+
100
+ ```ts
101
+ // Explicit:
102
+ export default defineConfig({
103
+ site: {...},
104
+ blog: { enabled: false },
105
+ });
106
+
107
+ // Or omit the key entirely:
108
+ export default defineConfig({
109
+ site: {...},
110
+ // no blog → blog module is disabled
111
+ });
112
+ ```
113
+
114
+ The checklist runner uses `isModuleEnabled()` to skip checks for disabled modules.
115
+
116
+ ## What must NOT be done
117
+
118
+ - **Don't keep old per-module config files alongside the new one.** v0.2.0 hard-breaks `codejitsu-images.config.mjs` and `codejitsu-llms.config.mjs`. Delete them.
119
+ - **Don't store secrets in this config.** It ships to the client in some module configurations (e.g. business info → schema.org). Use env vars for anything sensitive.
120
+ - **Don't put computed values that depend on runtime state.** The config loads once at CLI start. If you need dynamic values (e.g. fetched from an API), do it inside the CLI's invocation, not in the config.
121
+ - **Don't duplicate `site.url` in module configs.** All modules read it from `site.url`. Setting it once means changing it once.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Identity helper that types your `codejitsu.config.ts` export.
3
+ *
4
+ * @example
5
+ * // codejitsu.config.ts
6
+ * import { defineConfig } from '@ibalzam/codejitsu-core/config';
7
+ * export default defineConfig({ site: { url: '...', name: '...' } });
8
+ *
9
+ * @param {import('./types.js').CodejitsuConfig} config
10
+ * @returns {import('./types.js').CodejitsuConfig}
11
+ */
12
+ export function defineConfig(config) {
13
+ return config;
14
+ }
@@ -0,0 +1,5 @@
1
+ export type * from './types.js';
2
+ // @ts-expect-error - .mjs runtime resolves at use time
3
+ export { defineConfig } from './define.mjs';
4
+ // @ts-expect-error - .mjs runtime resolves at use time
5
+ export { loadConfig, isModuleEnabled } from './load.mjs';
@@ -0,0 +1,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+
5
+ const CANDIDATES = [
6
+ 'codejitsu.config.ts',
7
+ 'codejitsu.config.mts',
8
+ 'codejitsu.config.mjs',
9
+ 'codejitsu.config.js',
10
+ 'codejitsu.config.json',
11
+ ];
12
+
13
+ /**
14
+ * Loads the Codejitsu config from the current working directory.
15
+ *
16
+ * Search order:
17
+ * 1. `codejitsu.config.{ts,mts,mjs,js,json}` at cwd root.
18
+ * 2. `codejitsu` key in `package.json`.
19
+ *
20
+ * `.ts` and `.mts` files load via `jiti`. If `jiti` isn't installed, the
21
+ * loader warns once and falls through to other candidates.
22
+ *
23
+ * @param {string} [cwd=process.cwd()]
24
+ * @returns {Promise<import('./types.js').CodejitsuConfig>}
25
+ */
26
+ export async function loadConfig(cwd = process.cwd()) {
27
+ for (const name of CANDIDATES) {
28
+ const filePath = path.join(cwd, name);
29
+ if (!fs.existsSync(filePath)) continue;
30
+
31
+ if (name.endsWith('.json')) {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ }
34
+
35
+ if (name.endsWith('.ts') || name.endsWith('.mts')) {
36
+ const config = await loadWithJiti(filePath);
37
+ if (config) return config;
38
+ continue;
39
+ }
40
+
41
+ const mod = await import(pathToFileURL(filePath).href);
42
+ return mod.default ?? mod;
43
+ }
44
+
45
+ const pkgPath = path.join(cwd, 'package.json');
46
+ if (fs.existsSync(pkgPath)) {
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
48
+ if (pkg.codejitsu) return pkg.codejitsu;
49
+ }
50
+
51
+ throw new Error(
52
+ `No Codejitsu config found in ${cwd}. Create codejitsu.config.ts (or .mjs/.json) ` +
53
+ `at the site root, or add a "codejitsu" key to package.json.`
54
+ );
55
+ }
56
+
57
+ let jitiWarned = false;
58
+ async function loadWithJiti(filePath) {
59
+ try {
60
+ const jitiMod = await import('jiti');
61
+ const factory = jitiMod.createJiti ?? jitiMod.default ?? jitiMod;
62
+ if (typeof factory !== 'function') {
63
+ throw new Error('Unexpected jiti API shape.');
64
+ }
65
+ const jiti = factory(process.cwd(), { interopDefault: true });
66
+ const mod = await jiti.import(filePath, { default: true });
67
+ return mod;
68
+ } catch (err) {
69
+ if (!jitiWarned) {
70
+ const reason = err instanceof Error ? err.message : String(err);
71
+ console.warn(
72
+ `[@ibalzam/codejitsu-core] Could not load TypeScript config (${reason}). ` +
73
+ `Install \`jiti\` as a dev dependency, or rename to codejitsu.config.mjs.`
74
+ );
75
+ jitiWarned = true;
76
+ }
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Returns true if the named module is enabled in the config.
83
+ * @param {import('./types.js').CodejitsuConfig} config
84
+ * @param {'blog'|'seo'|'images'|'llms'|'deploy'} module
85
+ * @returns {boolean}
86
+ */
87
+ export function isModuleEnabled(config, module) {
88
+ const value = config[module];
89
+ if (value === undefined || value === false) return false;
90
+ if (typeof value === 'object' && value !== null && value.enabled === false) return false;
91
+ return true;
92
+ }