@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.
- package/CLAUDE.md +55 -39
- package/MIGRATIONS/0.2.0.md +166 -0
- package/README.md +25 -5
- package/checklist/bin/run.mjs +97 -55
- package/modules/blog/CLAUDE.md +105 -52
- package/modules/blog/src/collection.ts +176 -0
- package/modules/blog/src/fs.ts +167 -0
- package/modules/blog/src/index.ts +5 -201
- package/modules/blog/src/types.ts +71 -0
- package/modules/blog/templates/content.config.ts +27 -0
- package/modules/blog/templates/lib/blog-fs.ts +14 -0
- package/modules/blog/templates/lib/blog.ts +11 -3
- package/modules/config/CLAUDE.md +121 -0
- package/modules/config/src/define.mjs +14 -0
- package/modules/config/src/index.ts +5 -0
- package/modules/config/src/load.mjs +92 -0
- package/modules/config/src/types.ts +203 -0
- package/modules/images/CLAUDE.md +56 -39
- package/modules/images/bin/optimize.mjs +42 -34
- package/modules/images/checklist.md +15 -7
- package/modules/images/src/auto-blog.mjs +112 -0
- package/modules/images/src/index.ts +3 -18
- package/modules/images/src/optimize.mjs +7 -9
- package/modules/llms/CLAUDE.md +121 -28
- package/modules/llms/bin/generate.mjs +13 -23
- package/modules/llms/checklist.md +7 -6
- package/modules/llms/src/generate.mjs +374 -108
- package/modules/seo/CLAUDE.md +65 -21
- package/modules/seo/templates/Head.astro +99 -27
- package/package.json +11 -1
- package/src/index.ts +1 -1
- package/modules/images/templates/codejitsu-images.config.mjs +0 -18
- package/modules/llms/templates/codejitsu-llms.config.mjs +0 -39
package/CLAUDE.md
CHANGED
|
@@ -1,67 +1,83 @@
|
|
|
1
1
|
# Master instructions — @ibalzam/codejitsu-core
|
|
2
2
|
|
|
3
|
-
|
|
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 `
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
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
|
-
|
|
32
|
+
Non-negotiable unless the user explicitly opts out.
|
|
15
33
|
|
|
16
34
|
### Stack
|
|
17
|
-
- **Astro** (latest stable). Pure static
|
|
18
|
-
- **Tailwind v4** via `@tailwindcss/vite`. Theme via CSS variables
|
|
19
|
-
- **TypeScript** everywhere except
|
|
20
|
-
- **React** integration only if the site needs
|
|
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
|
-
-
|
|
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'
|
|
29
|
-
- Canonical URLs
|
|
46
|
+
- `trailingSlash: 'always'`. Internal links end with `/`.
|
|
47
|
+
- Canonical URLs absolute, trailing-slashed.
|
|
30
48
|
|
|
31
49
|
### Images
|
|
32
|
-
- Source images in `public
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
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 `
|
|
60
|
+
- `/llms.txt` + `/llms-full.txt` generated via `codejitsu-llms` in prebuild.
|
|
45
61
|
|
|
46
62
|
### Content
|
|
47
|
-
- Blog posts as
|
|
48
|
-
- Future-dated posts
|
|
49
|
-
-
|
|
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
|
|
53
|
-
- Don't hardcode brand colors in component files —
|
|
54
|
-
- Don't add
|
|
55
|
-
- Don't
|
|
56
|
-
- Don't
|
|
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
|
-
|
|
76
|
+
`npx codejitsu-check` and walk `checklist/core.md`. For UI: build, open in browser, verify visually.
|
|
61
77
|
|
|
62
|
-
##
|
|
78
|
+
## Upgrading `@ibalzam/codejitsu-core`
|
|
63
79
|
|
|
64
80
|
1. `npm update @ibalzam/codejitsu-core`
|
|
65
|
-
2. Read `
|
|
66
|
-
3. Apply migration steps in order. They're prose
|
|
67
|
-
4. Run `
|
|
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
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
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
|
|
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
|
|
package/checklist/bin/run.mjs
CHANGED
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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.mjs';
|
|
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
|
-
|
|
22
|
-
}
|
|
23
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 (
|
|
54
|
-
|
|
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
|
-
|
|
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 (
|
|
61
|
-
fail('
|
|
62
|
-
else
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 (
|
|
94
|
-
|
|
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 (
|
|
100
|
-
|
|
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) {
|
|
@@ -124,23 +163,20 @@ const webpSet = new Set();
|
|
|
124
163
|
}
|
|
125
164
|
})(distDir);
|
|
126
165
|
|
|
127
|
-
|
|
166
|
+
// Matches actual placeholder *content*, not CSS ::placeholder or HTML
|
|
167
|
+
// placeholder="..." attributes (both legitimate).
|
|
168
|
+
const PLACEHOLDER_RE = /\b(lorem ipsum|TODO:|FIXME:|XXX:)\b/i;
|
|
128
169
|
|
|
129
170
|
for (const file of htmlFiles) {
|
|
130
171
|
const rel = path.relative(distDir, file);
|
|
131
172
|
const html = readFile(file);
|
|
132
173
|
|
|
133
174
|
if (!/<title>[^<]+<\/title>/.test(html)) missingTitle.push(rel);
|
|
134
|
-
if (!/<meta\s+name=["']description["']\s+content=["'][^"']+["']/i.test(html))
|
|
135
|
-
|
|
136
|
-
if (!/<
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
175
|
+
if (!/<meta\s+name=["']description["']\s+content=["'][^"']+["']/i.test(html)) missingDescription.push(rel);
|
|
176
|
+
if (!/<link\s+rel=["']canonical["']\s+href=["'][^"']+["']/i.test(html)) missingCanonical.push(rel);
|
|
177
|
+
if (!/<meta\s+property=["']og:image["']\s+content=["'][^"']+["']/i.test(html)) missingOgImage.push(rel);
|
|
178
|
+
if (!/<script\s+type=["']application\/ld\+json["']/i.test(html)) missingJsonLd.push(rel);
|
|
179
|
+
|
|
144
180
|
for (const m of html.matchAll(/<img[^>]+src=["']([^"']+\.(?:png|jpe?g))["']/gi)) {
|
|
145
181
|
const src = m[1].replace(/^\//, '');
|
|
146
182
|
const noExt = src.replace(/\.(?:png|jpe?g)$/i, '');
|
|
@@ -148,6 +184,11 @@ for (const file of htmlFiles) {
|
|
|
148
184
|
}
|
|
149
185
|
|
|
150
186
|
if (PLACEHOLDER_RE.test(html)) placeholderText.push(rel);
|
|
187
|
+
|
|
188
|
+
// JSON-LD that contains an unescaped </ closing tag indicator (potential XSS).
|
|
189
|
+
for (const m of html.matchAll(/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
190
|
+
if (/<\/[a-z]/i.test(m[1])) unsafeJsonLd.push(rel);
|
|
191
|
+
}
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
function reportList(name, list, severity = 'fail') {
|
|
@@ -166,9 +207,10 @@ reportList('Every page has <title>', missingTitle);
|
|
|
166
207
|
reportList('Every page has <meta description>', missingDescription);
|
|
167
208
|
reportList('Every page has canonical link', missingCanonical);
|
|
168
209
|
reportList('Every page has og:image', missingOgImage, 'warn');
|
|
169
|
-
reportList('Every page has JSON-LD schema', missingJsonLd);
|
|
210
|
+
if (enabled.seo) reportList('Every page has JSON-LD schema', missingJsonLd);
|
|
170
211
|
reportList('No <img> references raw PNG/JPG where WebP exists', pngWhereWebp);
|
|
171
212
|
reportList('No placeholder text in production HTML', placeholderText);
|
|
213
|
+
reportList('JSON-LD uses safe injection (escapes </)', unsafeJsonLd);
|
|
172
214
|
|
|
173
215
|
// ─── Output ──────────────────────────────────────────────────────────────
|
|
174
216
|
|
|
@@ -180,7 +222,7 @@ function printAndExit() {
|
|
|
180
222
|
const fails = checks.filter((c) => c.status === 'fail').length;
|
|
181
223
|
|
|
182
224
|
for (const c of checks) {
|
|
183
|
-
const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '!' : '✗';
|
|
225
|
+
const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '!' : c.status === 'info' ? 'i' : '✗';
|
|
184
226
|
const line = `${icon} ${c.name}`;
|
|
185
227
|
console.log(c.detail ? `${line}\n ${c.detail}` : line);
|
|
186
228
|
}
|