@ibalzam/codejitsu-core 0.3.3 → 0.5.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.
@@ -198,6 +198,12 @@ async function generateContentScan({ config, cwd }) {
198
198
  const llms = config.llms;
199
199
  const scan = llms.contentScan ?? {};
200
200
 
201
+ // Date + draft field names come from the blog module config; CC schemas like
202
+ // pearl's use `pubDate` + `draft`, while simpler sites use `date` (+ no draft).
203
+ const blogCfg = config.blog && typeof config.blog === 'object' ? config.blog : {};
204
+ const dateField = blogCfg.dateField ?? 'date';
205
+ const draftField = blogCfg.draftField ?? null;
206
+
201
207
  const servicesDir = scan.servicesDir ? path.resolve(cwd, scan.servicesDir) : null;
202
208
  const locationsDir = scan.locationsDir ? path.resolve(cwd, scan.locationsDir) : null;
203
209
  const pagesDir = scan.pagesDir ? path.resolve(cwd, scan.pagesDir) : null;
@@ -205,10 +211,7 @@ async function generateContentScan({ config, cwd }) {
205
211
 
206
212
  const services = readContentDir(servicesDir);
207
213
  const locations = readContentDir(locationsDir);
208
- const blogPosts = readBlogPosts(blogDir, 'pubDate', 'draft').concat(
209
- // Also try 'date' field for fallback
210
- blogDir && readBlogPosts(blogDir, 'date', 'draft').filter((p) => !p.pubDate) || []
211
- );
214
+ const blogPosts = readBlogPosts(blogDir, dateField, draftField);
212
215
  const pages = pagesDir ? collectStaticPages(pagesDir) : [];
213
216
 
214
217
  const dynamicRoutes = scan.dynamicRoutes ?? [];
@@ -239,6 +242,7 @@ async function generateContentScan({ config, cwd }) {
239
242
  business: site.business,
240
243
  services,
241
244
  locations,
245
+ blogPosts: blogPosts.slice(0, llms.blogFullLimit ?? 20),
242
246
  aiGuidance: llms.aiGuidance,
243
247
  today: isoDate(),
244
248
  });
@@ -372,7 +376,7 @@ function renderContentScanConcise({ siteUrl, siteName, tagline, about, business,
372
376
  return lines.join('\n') + '\n';
373
377
  }
374
378
 
375
- function renderContentScanFull({ siteUrl, siteName, tagline, about, business, services, locations, aiGuidance, today }) {
379
+ function renderContentScanFull({ siteUrl, siteName, tagline, about, business, services, locations, blogPosts, aiGuidance, today }) {
376
380
  const lines = [];
377
381
  lines.push(`# ${siteName} — Full Reference`);
378
382
  lines.push(`Last Updated: ${today}`, '');
@@ -431,6 +435,19 @@ function renderContentScanFull({ siteUrl, siteName, tagline, about, business, se
431
435
  lines.push('', '---', '');
432
436
  }
433
437
 
438
+ if (blogPosts && blogPosts.length) {
439
+ lines.push('## Blog Posts', '');
440
+ for (const post of blogPosts) {
441
+ lines.push(`### ${post.title}`, '');
442
+ if (post.date) lines.push(`**Published**: ${post.date}`);
443
+ if (post.author) lines.push(`**Author**: ${post.author}`);
444
+ if (post.tags?.length) lines.push(`**Tags**: ${post.tags.join(', ')}`);
445
+ lines.push(`**URL**: ${siteUrl}/blog/${post.slug}/`, '');
446
+ if (post.description) lines.push(post.description, '');
447
+ lines.push('---', '');
448
+ }
449
+ }
450
+
434
451
  lines.push(`## Optional`, '', `- Sitemap: ${siteUrl}/sitemap-index.xml`, '');
435
452
  if (aiGuidance) lines.push('## For AI Assistants', '', aiGuidance, '');
436
453
  return lines.join('\n') + '\n';
@@ -0,0 +1,64 @@
1
+ # Rehype module — instructions for Claude
2
+
3
+ When the user asks to **fix trailing-slash bugs in markdown content** (or pre-empt them across a Codejitsu site), wire up `rehypeTrailingSlash`.
4
+
5
+ ## What this module provides
6
+
7
+ A single rehype plugin: `trailingSlash`. Runs during Astro's markdown→HTML conversion. Walks the HTML AST and rewrites internal `<a href="/foo">` to `<a href="/foo/">` (or vice versa with `policy: 'never'`).
8
+
9
+ **Why this exists:** Astro's `trailingSlash: 'always'` config covers route resolution and `Astro.url.pathname` but does NOT touch href strings written by humans in markdown or `.astro` files. This plugin closes that gap for markdown-rendered HTML.
10
+
11
+ It does **not** affect href strings inside `.astro` component source (Astro doesn't run rehype on those). Use the audit to catch those.
12
+
13
+ ## Wiring it into a site
14
+
15
+ ```ts
16
+ // astro.config.mjs
17
+ import { defineConfig } from 'astro/config';
18
+ import trailingSlash from '@ibalzam/codejitsu-core/rehype/trailing-slash';
19
+
20
+ export default defineConfig({
21
+ markdown: {
22
+ rehypePlugins: [trailingSlash],
23
+ },
24
+ });
25
+ ```
26
+
27
+ With explicit options:
28
+
29
+ ```ts
30
+ rehypePlugins: [
31
+ [trailingSlash, { policy: 'always' }],
32
+ ],
33
+ ```
34
+
35
+ ## What it does NOT touch
36
+
37
+ - External URLs (`http://`, `https://`, `//`)
38
+ - `mailto:`, `tel:`, `javascript:`
39
+ - Anchor-only links (`#section`)
40
+ - Paths ending in a file extension (`.pdf`, `.html`, `.webp`, etc.)
41
+ - Root path (`/`)
42
+
43
+ ## What it DOES touch
44
+
45
+ - `<a href="/foo">` → `<a href="/foo/">`
46
+ - `<a href="/foo?bar=1">` → `<a href="/foo/?bar=1">`
47
+ - `<a href="/foo#section">` → `<a href="/foo/#section">`
48
+
49
+ Preserves query strings and fragments. Path-only modification.
50
+
51
+ ## What must NOT be done
52
+
53
+ - **Don't apply this to `.astro` component files** — the plugin runs on markdown rehype, not Astro components. If a `<a href="/foo">` lives in a `.astro` file, the plugin can't see it.
54
+ - **Don't set `policy: 'never'` if `astro.config` has `trailingSlash: 'always'`** — they'd contradict each other. The audit will flag the inconsistency.
55
+ - **Don't run this with `policy: 'preserve'` and expect anything to change** — that mode is a no-op (registered as a placeholder for symmetry).
56
+
57
+ ## Verify after wiring
58
+
59
+ ```bash
60
+ npm run build
61
+ npx codejitsu audit
62
+ ```
63
+
64
+ The audit's "All internal links end with /" check should now report 0 markdown-level offenders. Component-level offenders (in `.astro` files) still surface — those must be fixed by hand.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Rehype plugin that enforces a trailing-slash policy on internal `<a href>`
3
+ * values produced by markdown content. Astro's `trailingSlash: 'always'`
4
+ * controls page routing but does NOT rewrite hand-written hrefs in markdown
5
+ * or component templates. This plugin fills that gap for markdown content.
6
+ *
7
+ * Usage in astro.config.mjs:
8
+ *
9
+ * import trailingSlash from '@ibalzam/codejitsu-core/rehype/trailing-slash';
10
+ *
11
+ * export default defineConfig({
12
+ * markdown: {
13
+ * rehypePlugins: [trailingSlash],
14
+ * },
15
+ * });
16
+ *
17
+ * Or with options:
18
+ *
19
+ * rehypePlugins: [[trailingSlash, { policy: 'always' }]]
20
+ *
21
+ * What it skips (leaves untouched):
22
+ * - External URLs (http://, https://, //, mailto:, tel:, etc.)
23
+ * - Anchor-only links (#section)
24
+ * - Paths ending in a file extension (.pdf, .html, .webp, ...)
25
+ * - Root path `/`
26
+ *
27
+ * @param {object} [opts]
28
+ * @param {'always' | 'never' | 'preserve'} [opts.policy='always']
29
+ */
30
+ export default function rehypeTrailingSlash(opts = {}) {
31
+ const policy = opts.policy ?? 'always';
32
+ if (policy === 'preserve') return () => {};
33
+
34
+ return (tree) => {
35
+ walk(tree, (node) => {
36
+ if (node.tagName !== 'a') return;
37
+ const href = node.properties?.href;
38
+ if (typeof href !== 'string') return;
39
+
40
+ const normalized = normalize(href, policy);
41
+ if (normalized !== href) {
42
+ node.properties.href = normalized;
43
+ }
44
+ });
45
+ };
46
+ }
47
+
48
+ function walk(node, fn) {
49
+ if (node?.type === 'element') fn(node);
50
+ if (Array.isArray(node?.children)) {
51
+ for (const child of node.children) walk(child, fn);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Apply policy to a single href. Pure, no side-effects — exported for tests.
57
+ *
58
+ * @param {string} href
59
+ * @param {'always' | 'never'} policy
60
+ */
61
+ export function normalize(href, policy) {
62
+ if (!href.startsWith('/')) return href; // external, anchor, relative — skip
63
+ if (href.startsWith('//')) return href; // protocol-relative external
64
+ if (href === '/') return href; // root is its own canonical
65
+
66
+ // Split path / query / fragment so we don't break /foo?bar=baz or /foo#anchor.
67
+ const m = href.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
68
+ if (!m) return href;
69
+ let path = m[1];
70
+ const query = m[2] ?? '';
71
+ const fragment = m[3] ?? '';
72
+
73
+ if (!path || path === '/') return href;
74
+
75
+ // Last segment with a `.` is likely a file (e.g. /robots.txt, /og-image.webp).
76
+ const lastSeg = path.split('/').filter(Boolean).pop() ?? '';
77
+ if (lastSeg.includes('.')) return href;
78
+
79
+ const endsWithSlash = path.endsWith('/');
80
+
81
+ if (policy === 'always' && !endsWithSlash) {
82
+ path = `${path}/`;
83
+ } else if (policy === 'never' && endsWithSlash) {
84
+ path = path.replace(/\/+$/, '');
85
+ }
86
+
87
+ return `${path}${query}${fragment}`;
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibalzam/codejitsu-core",
3
- "version": "0.3.3",
3
+ "version": "0.5.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": [
@@ -60,6 +60,7 @@
60
60
  "default": "./modules/seo/src/sitemap.js"
61
61
  },
62
62
  "./seo/Head.astro": "./modules/seo/templates/Head.astro",
63
+ "./rehype/trailing-slash": "./modules/rehype/src/trailing-slash.mjs",
63
64
  "./images": {
64
65
  "types": "./modules/images/src/index.d.ts",
65
66
  "default": "./modules/images/src/index.js"
@@ -68,6 +69,7 @@
68
69
  "./package.json": "./package.json"
69
70
  },
70
71
  "bin": {
72
+ "codejitsu": "./bin/codejitsu.mjs",
71
73
  "codejitsu-llms": "./modules/llms/bin/generate.mjs",
72
74
  "codejitsu-optimize-images": "./modules/images/bin/optimize.mjs",
73
75
  "codejitsu-check": "./checklist/bin/run.mjs"