@ibalzam/codejitsu-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,67 +1,83 @@
1
1
  # Master instructions — @ibalzam/codejitsu-core
2
2
 
3
- This is the shared core for all Codejitsu sites. When a Codejitsu site depends on this package and the user invokes a module by name (e.g. **"implement codejitsu/core/blog"**, **"add codejitsu/core/seo"**), this file is your starting point.
3
+ Shared core for every Codejitsu site. When the user invokes a module by name (e.g. **"implement codejitsu/core/blog"**), this file is your starting point.
4
4
 
5
5
  ## How to act on a module request
6
6
 
7
- 1. Open `node_modules/@ibalzam/codejitsu-core/modules/<name>/CLAUDE.md` it tells you what to do for that module specifically.
8
- 2. Import code from the matching subpath (e.g. `@ibalzam/codejitsu-core/blog`). Do **not** copy-paste the source into the site.
9
- 3. If the module has a `templates/` directory, copy those files into the site at the locations the module's CLAUDE.md specifies. Templates are starting points adapt them for the site's content.
10
- 4. After the change, run the module's `checklist.md` mentally, plus `checklist/core.md` (sitewide).
7
+ 1. **Always start with the unified config.** Open `codejitsu.config.ts` at the site root. If it doesn't exist yet, create one see `modules/config/CLAUDE.md`.
8
+ 2. Open `node_modules/@ibalzam/codejitsu-core/modules/<name>/CLAUDE.md` for that module's specific wiring instructions.
9
+ 3. Import code from the matching subpath (e.g. `@ibalzam/codejitsu-core/blog`). Do **not** copy-paste source into the site.
10
+ 4. If the module has a `templates/` directory, copy those files into the site at the locations the module's CLAUDE.md specifies. Adapt templates for the site's brand and content.
11
+ 5. After the change, walk through the module's `checklist.md` plus `checklist/core.md` (sitewide). Run `npx codejitsu-check` from the site root.
12
+
13
+ ## Module subpaths
14
+
15
+ | Subpath | Provides |
16
+ |---|---|
17
+ | `@ibalzam/codejitsu-core/config` | `defineConfig()`, `loadConfig()`, all types |
18
+ | `@ibalzam/codejitsu-core/blog` | `createBlog()` (fs+gray-matter), `createBlogFromCollection()` (Astro CC) |
19
+ | `@ibalzam/codejitsu-core/seo` | Schema builders, sitemap helpers, `jsonLd()` |
20
+ | `@ibalzam/codejitsu-core/seo/schema` | Schema builders only |
21
+ | `@ibalzam/codejitsu-core/seo/sitemap` | Sitemap helpers only |
22
+ | `@ibalzam/codejitsu-core/images` | `optimizeImages()`, `autoBlogImages()` (mostly used via CLI) |
23
+ | `@ibalzam/codejitsu-core/llms` | `generateLlms()` (mostly used via CLI) |
24
+
25
+ CLIs (auto-discover `codejitsu.config.ts`):
26
+ - `codejitsu-optimize-images`
27
+ - `codejitsu-llms`
28
+ - `codejitsu-check`
11
29
 
12
30
  ## Principles that apply to every Codejitsu site
13
31
 
14
- These are non-negotiable unless the user explicitly opts out:
32
+ Non-negotiable unless the user explicitly opts out.
15
33
 
16
34
  ### Stack
17
- - **Astro** (latest stable). Pure static output (`output: 'static'`).
18
- - **Tailwind v4** via `@tailwindcss/vite`. Theme via CSS variables on `:root`, not hardcoded color palettes.
19
- - **TypeScript** everywhere except config files where Astro/Vite expects `.mjs`.
20
- - **React** integration only if the site needs interactive client islands (Framer, charts). Otherwise pure Astro.
35
+ - **Astro** (latest stable). Pure static (`output: 'static'`).
36
+ - **Tailwind v4** via `@tailwindcss/vite`. Theme via CSS variables, not hardcoded palettes.
37
+ - **TypeScript** everywhere except where Astro/Vite expects `.mjs`.
38
+ - **React** integration only if the site needs client islands (Framer, charts). Otherwise pure Astro.
39
+ - **Astro Content Collections** for blog (use `createBlogFromCollection`). The fs loader is for non-Astro projects.
21
40
 
22
41
  ### Deploy
23
42
  - **Cloudflare Pages**, static deploy. `wrangler.toml` at site root.
24
- - `npm run build && npx wrangler pages deploy dist` is the deploy command (or git-integration on Pages).
25
- - Daily GH Action (`.github/workflows/daily-deploy.yml`) pings a Cloudflare deploy hook to publish scheduled content. See `modules/deploy/`.
43
+ - Daily GH Action pings the Cloudflare deploy hook to publish scheduled content.
26
44
 
27
45
  ### URLs + routing
28
- - `trailingSlash: 'always'` in Astro config. Every internal link ends with `/`.
29
- - Canonical URLs are absolute and trailing-slashed.
46
+ - `trailingSlash: 'always'`. Internal links end with `/`.
47
+ - Canonical URLs absolute, trailing-slashed.
30
48
 
31
49
  ### Images
32
- - Source images in `public/images/` (or `src/assets/` for Astro-processed). Originals can be PNG/JPG.
33
- - Every shipped image must be available as WebP. The Astro sharp service handles `<Image>` references automatically (`image.defaults: { quality: 82, format: 'webp' }`). For images referenced by URL in HTML/CSS, run the pre-pass: `npx codejitsu-optimize-images`.
34
- - No raw PNGs referenced from production HTML when a WebP equivalent exists.
35
-
36
- ### SEO (must be on every page)
37
- - `<title>` and `<meta name="description">` set per page.
38
- - Canonical URL `<link rel="canonical">`.
39
- - OG meta (`og:title`, `og:description`, `og:image`, `og:url`, `og:type`).
40
- - Twitter card meta.
41
- - JSON-LD schema.org appropriate to the page type (Organization on home, LocalBusiness if applicable, BlogPosting on blog posts, FAQPage if FAQs present). Use builders from `@ibalzam/codejitsu-core/seo/schema`.
42
- - `sitemap.xml` generated via `@astrojs/sitemap` with helpers from `@ibalzam/codejitsu-core/seo/sitemap`.
50
+ - Source images for general assets in `public/...`; auto-converted to WebP via `codejitsu-optimize-images` in `prebuild`.
51
+ - Blog source images live OUTSIDE `public/` (e.g. `private/blog-source-images/`), named `<slug>.{png,jpg,jpeg,webp}`. The CLI optimizes them to `public/assets/images/blog/<slug>.webp` via `autoBlogImages`.
52
+ - Astro `<Image>` auto-converts components-loaded images. Use it for hero / inline imagery imported from `src/assets/`.
53
+ - No raw PNG/JPG references in production HTML where a `.webp` sibling exists.
54
+
55
+ ### SEO (every page)
56
+ - `<title>`, meta description, canonical, OG, Twitter, JSON-LD via `<SiteHead />` (site wrapper around `@ibalzam/codejitsu-core/seo/Head.astro`).
57
+ - Schemas from `@ibalzam/codejitsu-core/seo` builders. Inject with `jsonLd()` (never raw `JSON.stringify`).
58
+ - `sitemap.xml` generated via `@astrojs/sitemap` + `defaultPriorityRules()` + `excludeFuturePosts()`.
43
59
  - `robots.txt` at site root.
44
- - `/llms.txt` + `/llms-full.txt` generated via `npx codejitsu-llms` in prebuild.
60
+ - `/llms.txt` + `/llms-full.txt` generated via `codejitsu-llms` in prebuild.
45
61
 
46
62
  ### Content
47
- - Blog posts as Markdown in `content/blog/` with frontmatter (see `modules/blog/CLAUDE.md`).
48
- - Future-dated posts are hidden from public pages and sitemap but kept addressable for OG meta scrapers.
49
- - All copy in English unless the site uses the i18n module (workzen-only currently).
63
+ - Blog posts as `.md` in `src/content/blog/` with frontmatter validated by an Astro CC schema.
64
+ - Future-dated posts hidden from public pages and sitemap; pages built for OG scrapers.
65
+ - `draft: true` posts excluded everywhere.
50
66
 
51
67
  ### What NOT to do
52
- - Don't reinvent modules that exist here. If you're tempted to write a blog loader, image optimizer, sitemap config, schema builder, or daily-deploy workflow, **import or copy from this package instead**.
53
- - Don't hardcode brand colors in component files — always via Tailwind theme tokens / CSS variables.
54
- - Don't add a runtime database, server-rendered routes, or anything that breaks static export. These sites deploy as plain HTML.
55
- - Don't add page-level redirects in code; use Cloudflare Pages `_redirects` or `_headers`.
56
- - Don't introduce a new heavy dependency without checking if existing modules already cover it.
68
+ - Don't reinvent modules. Always import or copy from this package.
69
+ - Don't hardcode brand colors in component files — use Tailwind theme tokens / CSS variables.
70
+ - Don't add server-rendered routes or anything that breaks static export.
71
+ - Don't write to `dist/` directly. All artifacts flow from `prebuild` + `astro build`.
72
+ - Don't keep old per-module config files (`codejitsu-images.config.mjs`, `codejitsu-llms.config.mjs`). Only `codejitsu.config.ts` is read in v0.2.0+.
57
73
 
58
74
  ## After any non-trivial change
59
75
 
60
- Run through `checklist/core.md` before reporting work as done. For UI changes, build the site and view the pages in a browser type-checking is not visual verification.
76
+ `npx codejitsu-check` and walk `checklist/core.md`. For UI: build, open in browser, verify visually.
61
77
 
62
- ## Updating to a new version of `@ibalzam/codejitsu-core`
78
+ ## Upgrading `@ibalzam/codejitsu-core`
63
79
 
64
80
  1. `npm update @ibalzam/codejitsu-core`
65
- 2. Read `node_modules/@ibalzam/codejitsu-core/MIGRATIONS/` for any version notes newer than the previous installed version.
66
- 3. Apply migration steps in order. They're prose, not codemods — judgment is fine; ask the user when ambiguous.
67
- 4. Run `checklist/core.md` to verify nothing regressed.
81
+ 2. Read every `MIGRATIONS/<version>.md` between the old installed version and the new one.
82
+ 3. Apply migration steps in order. They're prose — judgment is fine; ask the user when ambiguous.
83
+ 4. Run `codejitsu-check` to verify.
@@ -0,0 +1,166 @@
1
+ # 0.2.0 — Unified config, CC blog support, richer Head
2
+
3
+ ## Summary
4
+
5
+ v0.2.0 introduces a **single config file** (`codejitsu.config.ts`) that drives every module. The blog module gains Astro Content Collections support. The Head template gains noindex, hero preload, and article metadata. This is a **hard break** from v0.1.0 — old per-module config files are no longer read.
6
+
7
+ ## Required actions
8
+
9
+ ### 1. Create `codejitsu.config.ts` at the site root
10
+
11
+ ```ts
12
+ import { defineConfig } from '@ibalzam/codejitsu-core/config';
13
+
14
+ export default defineConfig({
15
+ site: {
16
+ url: 'https://example.com',
17
+ name: 'Example',
18
+ titleSuffix: ' | Example',
19
+ defaultAuthor: 'editor',
20
+ defaultOgImage: '/og-image.webp',
21
+ locale: 'en_US',
22
+ business: {
23
+ telephone: '...',
24
+ email: '...',
25
+ address: { addressLocality: 'City', addressCountry: 'US' },
26
+ // optionally: license, areaServed, sameAs, etc.
27
+ },
28
+ },
29
+
30
+ blog: {
31
+ mode: 'collection', // or 'fs' for non-Astro projects
32
+ collectionName: 'blog',
33
+ dateField: 'pubDate', // adjust to match your CC schema
34
+ draftField: 'draft',
35
+ },
36
+
37
+ seo: {
38
+ sitemap: {
39
+ excludePatterns: [/\/lp\//],
40
+ },
41
+ },
42
+
43
+ images: {
44
+ sourceDir: 'public/assets/images',
45
+ defaultQuality: 82,
46
+ defaultMaxSize: 1376,
47
+ specialRules: { /* ... */ },
48
+ autoBlogImages: {
49
+ contentDir: 'src/content/blog',
50
+ sourceImageDir: 'private/blog-source-images',
51
+ outputDir: 'public/assets/images/blog',
52
+ width: 1376,
53
+ },
54
+ },
55
+
56
+ llms: {
57
+ mode: 'content-scan',
58
+ tagline: '...',
59
+ about: '...',
60
+ aboutFull: '...',
61
+ aiGuidance: '...',
62
+ blogDir: 'src/content/blog',
63
+ contentScan: {
64
+ servicesDir: 'src/content/services',
65
+ locationsDir: 'src/content/locations',
66
+ pagesDir: 'src/pages',
67
+ dynamicRoutes: [
68
+ { template: '/services/{services}/' },
69
+ { template: '/services/{services}/{locations}/' },
70
+ { template: '/service-areas/{locations}/' },
71
+ ],
72
+ },
73
+ },
74
+
75
+ deploy: { cloudflarePagesName: 'example' },
76
+ });
77
+ ```
78
+
79
+ ### 2. Install jiti as a dev dep (for `.ts` config)
80
+
81
+ ```bash
82
+ npm install -D jiti
83
+ ```
84
+
85
+ `.mjs` and `.json` configs work without it. Skip if you used those formats.
86
+
87
+ ### 3. Delete deprecated config files
88
+
89
+ ```bash
90
+ rm -f codejitsu-images.config.mjs codejitsu-llms.config.mjs
91
+ ```
92
+
93
+ `codejitsu-check` flags these as failures in v0.2.0.
94
+
95
+ ### 4. Update blog wiring (if you used the v0.1.0 fs loader)
96
+
97
+ **Before (v0.1.0):**
98
+ ```ts
99
+ // src/lib/blog.ts
100
+ import { createBlog } from '@ibalzam/codejitsu-core/blog';
101
+ export const blog = createBlog({ contentDir: 'content/blog', defaultAuthor: 'editor' });
102
+ ```
103
+
104
+ **After (v0.2.0, Astro CC):**
105
+ ```ts
106
+ // src/lib/blog.ts
107
+ import { createBlogFromCollection } from '@ibalzam/codejitsu-core/blog';
108
+ export const blog = createBlogFromCollection({
109
+ collectionName: 'blog',
110
+ dateField: 'pubDate', // match your CC schema
111
+ draftField: 'draft',
112
+ defaultAuthor: 'editor',
113
+ });
114
+ ```
115
+
116
+ If you stayed on fs mode: `createBlog` still works, but it now reads `dateField`/`draftField` config. Pass them if your frontmatter uses non-default field names.
117
+
118
+ ### 5. Update Head wiring
119
+
120
+ The package's `Head.astro` template grew new props (`siteUrl`, `siteName`, `locale`, `titleSuffix`, `heroImage`, `author`, `publishedTime`, `modifiedTime`). Sites should wrap it in `SiteHead.astro` that passes config defaults. See `modules/seo/CLAUDE.md`.
121
+
122
+ ### 6. Replace `JSON.stringify(schema)` with `jsonLd(schema)`
123
+
124
+ If any layout/component injects schema via `set:html={JSON.stringify(s)}`, replace with `jsonLd()` to escape `</` (prevents XSS via crafted schema content):
125
+
126
+ ```diff
127
+ + import { jsonLd } from '@ibalzam/codejitsu-core/seo';
128
+ ...
129
+ - <script type="application/ld+json" set:html={JSON.stringify(s)} />
130
+ + <script type="application/ld+json" set:html={jsonLd(s)} />
131
+ ```
132
+
133
+ `codejitsu-check` now flags pages where rendered JSON-LD contains unescaped `</`.
134
+
135
+ ### 7. Update `package.json` scripts (no change to commands)
136
+
137
+ Scripts stay the same — `codejitsu-optimize-images` and `codejitsu-llms` now auto-discover `codejitsu.config.ts` so the CLIs work as before.
138
+
139
+ ## Verify
140
+
141
+ After applying:
142
+
143
+ ```bash
144
+ rm -rf node_modules package-lock.json
145
+ npm install
146
+ npm run build
147
+ npx codejitsu-check
148
+ ```
149
+
150
+ All checks should pass. New v0.2.0 checks:
151
+ - `codejitsu config loaded` — config file exists and parses
152
+ - `No deprecated codejitsu-images.config.mjs` — old per-module config removed
153
+ - `No deprecated codejitsu-llms.config.mjs` — old per-module config removed
154
+ - `JSON-LD uses safe injection (escapes </)` — verifies `jsonLd()` is used
155
+
156
+ ## Rollback
157
+
158
+ If something goes badly:
159
+
160
+ ```bash
161
+ npm install @ibalzam/codejitsu-core@0.1.0
162
+ git checkout HEAD~1 -- codejitsu.config.ts # if it was committed
163
+ # Restore old codejitsu-images.config.mjs / codejitsu-llms.config.mjs from git
164
+ ```
165
+
166
+ But the v0.1.0 → v0.2.0 changes are mostly additive (new config file, new schemas) — applying them carefully should not break a working v0.1.0 setup. Roll forward, not back.
package/README.md CHANGED
@@ -11,13 +11,33 @@ Sites stay thin: configuration + content + brand. Everything else lives here.
11
11
 
12
12
  | Module | Subpath | What it provides |
13
13
  |---|---|---|
14
- | `blog` | `@ibalzam/codejitsu-core/blog` | Markdown-based blog system with scheduled publishing, FAQs, tags, categories |
15
- | `seo` | `@ibalzam/codejitsu-core/seo` | Sitemap helpers, schema.org JSON-LD builders, meta tag patterns |
16
- | `images` | `@ibalzam/codejitsu-core/images` + `codejitsu-optimize-images` CLI | PNG/JPG→WebP pre-pass + Astro sharp defaults |
14
+ | `config` | `@ibalzam/codejitsu-core/config` | `defineConfig()` + types for the unified `codejitsu.config.ts` |
15
+ | `blog` | `@ibalzam/codejitsu-core/blog` | `createBlog()` (fs+gray-matter) and `createBlogFromCollection()` (Astro CC) with scheduled publishing, drafts, dual-slug, FAQs, tags, categories |
16
+ | `seo` | `@ibalzam/codejitsu-core/seo` | Sitemap helpers, schema.org JSON-LD builders, safe `jsonLd()` injection, `Head.astro` template |
17
+ | `images` | `codejitsu-optimize-images` CLI | PNG/JPG→WebP optimizer + `autoBlogImages` (one image per post slug) |
17
18
  | `deploy` | (templates only) | GH Action daily-deploy + Cloudflare wrangler templates |
18
- | `llms` | `codejitsu-llms` CLI | Generates `/llms.txt` + `/llms-full.txt` from site content |
19
+ | `llms` | `codejitsu-llms` CLI | Generates `/llms.txt` + `/llms-full.txt`. Config-driven or content-scan modes |
19
20
 
20
- Plus `checklist/` — sitewide invariants Claude verifies after any non-trivial change.
21
+ Plus `checklist/` — sitewide invariants Claude verifies after any non-trivial change (`codejitsu-check` CLI).
22
+
23
+ ## Unified config
24
+
25
+ Every module reads from one file at the site root: `codejitsu.config.ts` (or `.mjs`, `.json`, or `codejitsu` key in `package.json`).
26
+
27
+ ```ts
28
+ import { defineConfig } from '@ibalzam/codejitsu-core/config';
29
+
30
+ export default defineConfig({
31
+ site: { url: 'https://...', name: '...', business: { /* ... */ } },
32
+ blog: { mode: 'collection', dateField: 'pubDate', draftField: 'draft' },
33
+ seo: { sitemap: { excludePatterns: [/\/lp\//] } },
34
+ images: { /* ... */ },
35
+ llms: { mode: 'content-scan', /* ... */ },
36
+ deploy: { /* ... */ },
37
+ });
38
+ ```
39
+
40
+ See `modules/config/CLAUDE.md` for the full shape.
21
41
 
22
42
  ## How Claude uses this package
23
43
 
@@ -1,35 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Smoke checker for Codejitsu sites. Run from a site repo root after
4
- * `npm run build`. Verifies sitewide invariants from `checklist/core.md`
5
- * that can be checked programmatically.
3
+ * Sitewide checker for Codejitsu sites. Run from the site repo root.
6
4
  *
7
- * Exit code 0 = all checks pass.
8
- * Exit code 1 = at least one check failed (warnings still exit 0).
5
+ * Reads `codejitsu.config.ts` (etc.) to know which modules are enabled,
6
+ * then runs programmatic checks against the source tree and `dist/`.
7
+ *
8
+ * Exit code 0 = no failures. Exit code 1 = at least one failure.
9
9
  *
10
10
  * Not exhaustive — visual/UX/content checks need human + Claude review.
11
11
  */
12
12
  import fs from 'fs';
13
13
  import path from 'path';
14
+ import { loadConfig, isModuleEnabled } from '../../modules/config/src/load.js';
14
15
 
15
16
  const cwd = process.cwd();
16
17
  const distDir = path.join(cwd, 'dist');
17
18
 
18
19
  const checks = [];
19
20
 
20
- function pass(name) {
21
- checks.push({ status: 'pass', name });
22
- }
23
- function warn(name, detail) {
24
- checks.push({ status: 'warn', name, detail });
25
- }
26
- function fail(name, detail) {
27
- checks.push({ status: 'fail', name, detail });
28
- }
21
+ function pass(name) { checks.push({ status: 'pass', name }); }
22
+ function warn(name, detail) { checks.push({ status: 'warn', name, detail }); }
23
+ function fail(name, detail) { checks.push({ status: 'fail', name, detail }); }
24
+ function info(name) { checks.push({ status: 'info', name }); }
29
25
 
30
- function exists(p) {
31
- return fs.existsSync(path.join(cwd, p));
32
- }
26
+ function exists(p) { return fs.existsSync(path.join(cwd, p)); }
27
+
28
+ function readFile(p) { return fs.readFileSync(p, 'utf8'); }
33
29
 
34
30
  function distHtmlFiles() {
35
31
  if (!fs.existsSync(distDir)) return [];
@@ -44,25 +40,57 @@ function distHtmlFiles() {
44
40
  return out;
45
41
  }
46
42
 
47
- function readFile(p) {
48
- return fs.readFileSync(p, 'utf8');
43
+ // ─── Config presence ─────────────────────────────────────────────────────
44
+
45
+ let config = null;
46
+ try {
47
+ config = await loadConfig(cwd);
48
+ pass('codejitsu config loaded');
49
+ } catch (err) {
50
+ fail('codejitsu config loaded', err.message);
51
+ printAndExit();
49
52
  }
50
53
 
51
- // ─── Pre-build files ─────────────────────────────────────────────────────
54
+ const enabled = {
55
+ blog: isModuleEnabled(config, 'blog'),
56
+ seo: isModuleEnabled(config, 'seo'),
57
+ images: isModuleEnabled(config, 'images'),
58
+ llms: isModuleEnabled(config, 'llms'),
59
+ deploy: isModuleEnabled(config, 'deploy'),
60
+ };
61
+
62
+ info(`Modules: ${Object.entries(enabled).filter(([, v]) => v).map(([k]) => k).join(', ') || 'none'}`);
63
+
64
+ // ─── Deprecated v0.1.0 config files ─────────────────────────────────────
65
+
66
+ if (exists('codejitsu-images.config.mjs')) {
67
+ fail('No deprecated codejitsu-images.config.mjs', 'Remove this file. v0.2.0 reads only codejitsu.config.ts.');
68
+ } else pass('No deprecated codejitsu-images.config.mjs');
52
69
 
53
- if (!exists('wrangler.toml')) fail('wrangler.toml present');
54
- else pass('wrangler.toml present');
70
+ if (exists('codejitsu-llms.config.mjs')) {
71
+ fail('No deprecated codejitsu-llms.config.mjs', 'Remove this file. v0.2.0 reads only codejitsu.config.ts.');
72
+ } else pass('No deprecated codejitsu-llms.config.mjs');
55
73
 
56
- if (!exists('.github/workflows/daily-deploy.yml'))
57
- warn('.github/workflows/daily-deploy.yml present', 'Skip only if site has no scheduled content.');
58
- else pass('.github/workflows/daily-deploy.yml present');
74
+ // ─── Pre-build invariants ────────────────────────────────────────────────
59
75
 
60
- if (!exists('astro.config.mjs') && !exists('astro.config.ts'))
61
- fail('astro.config.{mjs,ts} present');
62
- else {
63
- const cfg = readFile(
64
- path.join(cwd, exists('astro.config.ts') ? 'astro.config.ts' : 'astro.config.mjs')
65
- );
76
+ if (enabled.deploy) {
77
+ if (!exists('wrangler.toml')) fail('wrangler.toml present');
78
+ else pass('wrangler.toml present');
79
+
80
+ if (!exists('.github/workflows/daily-deploy.yml'))
81
+ warn('.github/workflows/daily-deploy.yml present', 'Skip only if site has no scheduled content.');
82
+ else pass('.github/workflows/daily-deploy.yml present');
83
+ }
84
+
85
+ const astroCfgPath = exists('astro.config.ts')
86
+ ? path.join(cwd, 'astro.config.ts')
87
+ : exists('astro.config.mjs')
88
+ ? path.join(cwd, 'astro.config.mjs')
89
+ : null;
90
+ if (!astroCfgPath) {
91
+ fail('astro.config.{ts,mjs} present');
92
+ } else {
93
+ const cfg = readFile(astroCfgPath);
66
94
  if (!/trailingSlash:\s*['"]always['"]/.test(cfg))
67
95
  fail("trailingSlash: 'always' in astro.config", 'Required for canonical URL policy.');
68
96
  else pass("trailingSlash: 'always' in astro.config");
@@ -71,15 +99,21 @@ else {
71
99
  fail("output: 'static' in astro.config");
72
100
  else pass("output: 'static' in astro.config");
73
101
 
74
- if (!/format:\s*['"]webp['"]/.test(cfg))
102
+ if (enabled.images && !/format:\s*['"]webp['"]/.test(cfg))
75
103
  warn("image.defaults.format: 'webp' in astro.config");
76
- else pass("image.defaults.format: 'webp' in astro.config");
104
+ else if (enabled.images) pass("image.defaults.format: 'webp' in astro.config");
105
+ }
106
+
107
+ if (enabled.seo) {
108
+ if (!exists('src/components/SiteHead.astro') && !exists('src/components/Head.astro'))
109
+ warn('Head component present', 'Recommended: src/components/SiteHead.astro wrapping @ibalzam/codejitsu-core/seo/Head.astro.');
110
+ else pass('Head component present');
77
111
  }
78
112
 
79
113
  // ─── Build artifacts ─────────────────────────────────────────────────────
80
114
 
81
115
  if (!fs.existsSync(distDir)) {
82
- fail('dist/ exists', 'Run `npm run build` first.');
116
+ warn('dist/ exists', 'Run `npm run build` first to enable per-page HTML checks.');
83
117
  printAndExit();
84
118
  }
85
119
  pass('dist/ exists');
@@ -90,17 +124,21 @@ else pass(`dist/ contains ${htmlFiles.length} HTML files`);
90
124
 
91
125
  const hasSitemap = fs.existsSync(path.join(distDir, 'sitemap-index.xml')) ||
92
126
  fs.existsSync(path.join(distDir, 'sitemap-0.xml'));
93
- if (!hasSitemap) fail('sitemap-(index|0).xml in dist/');
94
- else pass('sitemap in dist/');
127
+ if (enabled.seo) {
128
+ if (!hasSitemap) fail('sitemap-(index|0).xml in dist/');
129
+ else pass('sitemap in dist/');
95
130
 
96
- if (!fs.existsSync(path.join(distDir, 'robots.txt'))) fail('dist/robots.txt');
97
- else pass('dist/robots.txt');
131
+ if (!fs.existsSync(path.join(distDir, 'robots.txt'))) fail('dist/robots.txt');
132
+ else pass('dist/robots.txt');
133
+ }
98
134
 
99
- if (!fs.existsSync(path.join(distDir, 'llms.txt'))) warn('dist/llms.txt');
100
- else pass('dist/llms.txt');
135
+ if (enabled.llms) {
136
+ if (!fs.existsSync(path.join(distDir, 'llms.txt'))) warn('dist/llms.txt');
137
+ else pass('dist/llms.txt');
101
138
 
102
- if (!fs.existsSync(path.join(distDir, 'llms-full.txt'))) warn('dist/llms-full.txt');
103
- else pass('dist/llms-full.txt');
139
+ if (!fs.existsSync(path.join(distDir, 'llms-full.txt'))) warn('dist/llms-full.txt');
140
+ else pass('dist/llms-full.txt');
141
+ }
104
142
 
105
143
  // ─── Per-page checks ─────────────────────────────────────────────────────
106
144
 
@@ -111,6 +149,7 @@ const missingOgImage = [];
111
149
  const missingJsonLd = [];
112
150
  const pngWhereWebp = [];
113
151
  const placeholderText = [];
152
+ const unsafeJsonLd = [];
114
153
 
115
154
  const webpSet = new Set();
116
155
  (function walkAssets(dir) {
@@ -131,16 +170,11 @@ for (const file of htmlFiles) {
131
170
  const html = readFile(file);
132
171
 
133
172
  if (!/<title>[^<]+<\/title>/.test(html)) missingTitle.push(rel);
134
- if (!/<meta\s+name=["']description["']\s+content=["'][^"']+["']/i.test(html))
135
- missingDescription.push(rel);
136
- if (!/<link\s+rel=["']canonical["']\s+href=["'][^"']+["']/i.test(html))
137
- missingCanonical.push(rel);
138
- if (!/<meta\s+property=["']og:image["']\s+content=["'][^"']+["']/i.test(html))
139
- missingOgImage.push(rel);
140
- if (!/<script\s+type=["']application\/ld\+json["']/i.test(html))
141
- missingJsonLd.push(rel);
142
-
143
- // Find <img src="*.png|*.jpg"> and check if a .webp equivalent exists.
173
+ if (!/<meta\s+name=["']description["']\s+content=["'][^"']+["']/i.test(html)) missingDescription.push(rel);
174
+ if (!/<link\s+rel=["']canonical["']\s+href=["'][^"']+["']/i.test(html)) missingCanonical.push(rel);
175
+ if (!/<meta\s+property=["']og:image["']\s+content=["'][^"']+["']/i.test(html)) missingOgImage.push(rel);
176
+ if (!/<script\s+type=["']application\/ld\+json["']/i.test(html)) missingJsonLd.push(rel);
177
+
144
178
  for (const m of html.matchAll(/<img[^>]+src=["']([^"']+\.(?:png|jpe?g))["']/gi)) {
145
179
  const src = m[1].replace(/^\//, '');
146
180
  const noExt = src.replace(/\.(?:png|jpe?g)$/i, '');
@@ -148,6 +182,11 @@ for (const file of htmlFiles) {
148
182
  }
149
183
 
150
184
  if (PLACEHOLDER_RE.test(html)) placeholderText.push(rel);
185
+
186
+ // JSON-LD that contains an unescaped </ closing tag indicator (potential XSS).
187
+ for (const m of html.matchAll(/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)) {
188
+ if (/<\/[a-z]/i.test(m[1])) unsafeJsonLd.push(rel);
189
+ }
151
190
  }
152
191
 
153
192
  function reportList(name, list, severity = 'fail') {
@@ -166,9 +205,10 @@ reportList('Every page has <title>', missingTitle);
166
205
  reportList('Every page has <meta description>', missingDescription);
167
206
  reportList('Every page has canonical link', missingCanonical);
168
207
  reportList('Every page has og:image', missingOgImage, 'warn');
169
- reportList('Every page has JSON-LD schema', missingJsonLd);
208
+ if (enabled.seo) reportList('Every page has JSON-LD schema', missingJsonLd);
170
209
  reportList('No <img> references raw PNG/JPG where WebP exists', pngWhereWebp);
171
210
  reportList('No placeholder text in production HTML', placeholderText);
211
+ reportList('JSON-LD uses safe injection (escapes </)', unsafeJsonLd);
172
212
 
173
213
  // ─── Output ──────────────────────────────────────────────────────────────
174
214
 
@@ -180,7 +220,7 @@ function printAndExit() {
180
220
  const fails = checks.filter((c) => c.status === 'fail').length;
181
221
 
182
222
  for (const c of checks) {
183
- const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '!' : '✗';
223
+ const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '!' : c.status === 'info' ? 'i' : '✗';
184
224
  const line = `${icon} ${c.name}`;
185
225
  console.log(c.detail ? `${line}\n ${c.detail}` : line);
186
226
  }