@ibalzam/codejitsu-core 0.1.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.
Files changed (40) hide show
  1. package/CLAUDE.md +67 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATIONS/README.md +30 -0
  4. package/README.md +35 -0
  5. package/checklist/bin/run.mjs +189 -0
  6. package/checklist/core.md +55 -0
  7. package/modules/blog/CLAUDE.md +87 -0
  8. package/modules/blog/checklist.md +36 -0
  9. package/modules/blog/src/components/index.ts +1 -0
  10. package/modules/blog/src/index.ts +201 -0
  11. package/modules/blog/templates/content/_sample-post.md +18 -0
  12. package/modules/blog/templates/lib/blog.ts +17 -0
  13. package/modules/blog/templates/pages/blog/[...slug].astro +53 -0
  14. package/modules/blog/templates/pages/blog/category/[category].astro +40 -0
  15. package/modules/blog/templates/pages/blog/index.astro +30 -0
  16. package/modules/blog/templates/pages/blog/tag/[tag].astro +35 -0
  17. package/modules/deploy/CLAUDE.md +58 -0
  18. package/modules/deploy/checklist.md +9 -0
  19. package/modules/deploy/templates/daily-deploy.yml +29 -0
  20. package/modules/deploy/templates/wrangler.toml +2 -0
  21. package/modules/images/CLAUDE.md +77 -0
  22. package/modules/images/bin/optimize.mjs +44 -0
  23. package/modules/images/checklist.md +23 -0
  24. package/modules/images/src/index.ts +21 -0
  25. package/modules/images/src/optimize.mjs +113 -0
  26. package/modules/images/templates/codejitsu-images.config.mjs +18 -0
  27. package/modules/llms/CLAUDE.md +57 -0
  28. package/modules/llms/bin/generate.mjs +35 -0
  29. package/modules/llms/checklist.md +10 -0
  30. package/modules/llms/src/generate.mjs +179 -0
  31. package/modules/llms/templates/codejitsu-llms.config.mjs +39 -0
  32. package/modules/seo/CLAUDE.md +95 -0
  33. package/modules/seo/checklist.md +30 -0
  34. package/modules/seo/src/index.ts +2 -0
  35. package/modules/seo/src/schema.ts +203 -0
  36. package/modules/seo/src/sitemap.ts +109 -0
  37. package/modules/seo/templates/Head.astro +53 -0
  38. package/modules/seo/templates/robots.txt +5 -0
  39. package/package.json +73 -0
  40. package/src/index.ts +1 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,67 @@
1
+ # Master instructions — @ibalzam/codejitsu-core
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.
4
+
5
+ ## How to act on a module request
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).
11
+
12
+ ## Principles that apply to every Codejitsu site
13
+
14
+ These are non-negotiable unless the user explicitly opts out:
15
+
16
+ ### 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.
21
+
22
+ ### Deploy
23
+ - **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/`.
26
+
27
+ ### URLs + routing
28
+ - `trailingSlash: 'always'` in Astro config. Every internal link ends with `/`.
29
+ - Canonical URLs are absolute and trailing-slashed.
30
+
31
+ ### 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`.
43
+ - `robots.txt` at site root.
44
+ - `/llms.txt` + `/llms-full.txt` generated via `npx codejitsu-llms` in prebuild.
45
+
46
+ ### 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).
50
+
51
+ ### 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.
57
+
58
+ ## After any non-trivial change
59
+
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.
61
+
62
+ ## Updating to a new version of `@ibalzam/codejitsu-core`
63
+
64
+ 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ika Balzam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # Migrations
2
+
3
+ When `@ibalzam/codejitsu-core` ships a change that requires touching site-owned files (page routes, config, content), the change is described here as prose Claude reads and applies.
4
+
5
+ ## Format
6
+
7
+ One file per version: `MIGRATIONS/<version>.md`. Example: `MIGRATIONS/0.2.0.md`.
8
+
9
+ Each file follows this structure:
10
+
11
+ ```markdown
12
+ # 0.2.0
13
+
14
+ ## Summary
15
+ One paragraph: what changed and why.
16
+
17
+ ## Required actions
18
+ Numbered list of concrete things to do in the site. Reference file paths and code snippets. Be specific enough that Claude can apply them without ambiguity.
19
+
20
+ ## Verify
21
+ What to check after applying.
22
+ ```
23
+
24
+ ## Why prose, not codemods
25
+
26
+ We do migrations through Claude reading the notes, not jscodeshift. Trade-off: prose is more flexible (handles edge cases, branding variations, half-applied previous migrations) but requires Claude to actually be in the loop on the upgrade. That matches how this package is meant to be used — every Codejitsu site is maintained with Claude, so Claude can read and apply.
27
+
28
+ ## When NOT to write a migration
29
+
30
+ Most upgrades don't need one. If a change can live entirely in library code (a new component, a fixed function, an Astro integration hook that injects something automatically), ship it as a package update and let `npm update` propagate. Only write a migration when site-owned files must change.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @ibalzam/codejitsu-core
2
+
3
+ Shared core for all Codejitsu sites. Two channels per module:
4
+
5
+ - **Code** — importable via `@ibalzam/codejitsu-core/<module>` (Astro/TS).
6
+ - **Instructions** — a `CLAUDE.md` per module that tells Claude how to wire it into a site, what to do, what to avoid.
7
+
8
+ Sites stay thin: configuration + content + brand. Everything else lives here.
9
+
10
+ ## Modules
11
+
12
+ | Module | Subpath | What it provides |
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 |
17
+ | `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
+
20
+ Plus `checklist/` — sitewide invariants Claude verifies after any non-trivial change.
21
+
22
+ ## How Claude uses this package
23
+
24
+ In a site that depends on `@ibalzam/codejitsu-core`, when the user says **"implement codejitsu/core/blog"** (or animations, or deploy, etc.), Claude reads `node_modules/@ibalzam/codejitsu-core/modules/<name>/CLAUDE.md` for instructions, imports code from the matching subpath, copies any templates, and runs the module's `checklist.md` to verify the result.
25
+
26
+ Start at `CLAUDE.md` (the master entry).
27
+
28
+ ## Updating a site after a core release
29
+
30
+ ```bash
31
+ npm update @ibalzam/codejitsu-core
32
+ # Then tell Claude: "we just upgraded codejitsu/core, check MIGRATIONS for anything that needs applying"
33
+ ```
34
+
35
+ Migration notes live in `MIGRATIONS/<version>.md` as prose Claude reads and applies (not jscodeshift codemods — Claude does the work).
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
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.
6
+ *
7
+ * Exit code 0 = all checks pass.
8
+ * Exit code 1 = at least one check failed (warnings still exit 0).
9
+ *
10
+ * Not exhaustive — visual/UX/content checks need human + Claude review.
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+
15
+ const cwd = process.cwd();
16
+ const distDir = path.join(cwd, 'dist');
17
+
18
+ const checks = [];
19
+
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
+ }
29
+
30
+ function exists(p) {
31
+ return fs.existsSync(path.join(cwd, p));
32
+ }
33
+
34
+ function distHtmlFiles() {
35
+ if (!fs.existsSync(distDir)) return [];
36
+ const out = [];
37
+ (function walk(dir) {
38
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
39
+ const full = path.join(dir, entry.name);
40
+ if (entry.isDirectory()) walk(full);
41
+ else if (entry.name.endsWith('.html')) out.push(full);
42
+ }
43
+ })(distDir);
44
+ return out;
45
+ }
46
+
47
+ function readFile(p) {
48
+ return fs.readFileSync(p, 'utf8');
49
+ }
50
+
51
+ // ─── Pre-build files ─────────────────────────────────────────────────────
52
+
53
+ if (!exists('wrangler.toml')) fail('wrangler.toml present');
54
+ else pass('wrangler.toml present');
55
+
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');
59
+
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
+ );
66
+ if (!/trailingSlash:\s*['"]always['"]/.test(cfg))
67
+ fail("trailingSlash: 'always' in astro.config", 'Required for canonical URL policy.');
68
+ else pass("trailingSlash: 'always' in astro.config");
69
+
70
+ if (!/output:\s*['"]static['"]/.test(cfg))
71
+ fail("output: 'static' in astro.config");
72
+ else pass("output: 'static' in astro.config");
73
+
74
+ if (!/format:\s*['"]webp['"]/.test(cfg))
75
+ warn("image.defaults.format: 'webp' in astro.config");
76
+ else pass("image.defaults.format: 'webp' in astro.config");
77
+ }
78
+
79
+ // ─── Build artifacts ─────────────────────────────────────────────────────
80
+
81
+ if (!fs.existsSync(distDir)) {
82
+ fail('dist/ exists', 'Run `npm run build` first.');
83
+ printAndExit();
84
+ }
85
+ pass('dist/ exists');
86
+
87
+ const htmlFiles = distHtmlFiles();
88
+ if (htmlFiles.length === 0) fail('dist/ contains HTML files');
89
+ else pass(`dist/ contains ${htmlFiles.length} HTML files`);
90
+
91
+ const hasSitemap = fs.existsSync(path.join(distDir, 'sitemap-index.xml')) ||
92
+ fs.existsSync(path.join(distDir, 'sitemap-0.xml'));
93
+ if (!hasSitemap) fail('sitemap-(index|0).xml in dist/');
94
+ else pass('sitemap in dist/');
95
+
96
+ if (!fs.existsSync(path.join(distDir, 'robots.txt'))) fail('dist/robots.txt');
97
+ else pass('dist/robots.txt');
98
+
99
+ if (!fs.existsSync(path.join(distDir, 'llms.txt'))) warn('dist/llms.txt');
100
+ else pass('dist/llms.txt');
101
+
102
+ if (!fs.existsSync(path.join(distDir, 'llms-full.txt'))) warn('dist/llms-full.txt');
103
+ else pass('dist/llms-full.txt');
104
+
105
+ // ─── Per-page checks ─────────────────────────────────────────────────────
106
+
107
+ const missingTitle = [];
108
+ const missingDescription = [];
109
+ const missingCanonical = [];
110
+ const missingOgImage = [];
111
+ const missingJsonLd = [];
112
+ const pngWhereWebp = [];
113
+ const placeholderText = [];
114
+
115
+ const webpSet = new Set();
116
+ (function walkAssets(dir) {
117
+ if (!fs.existsSync(dir)) return;
118
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
119
+ const full = path.join(dir, entry.name);
120
+ if (entry.isDirectory()) walkAssets(full);
121
+ else if (entry.name.toLowerCase().endsWith('.webp')) {
122
+ webpSet.add(path.relative(distDir, full).replace(/\.webp$/i, ''));
123
+ }
124
+ }
125
+ })(distDir);
126
+
127
+ const PLACEHOLDER_RE = /\b(lorem ipsum|TODO|FIXME|XXX:|placeholder)\b/i;
128
+
129
+ for (const file of htmlFiles) {
130
+ const rel = path.relative(distDir, file);
131
+ const html = readFile(file);
132
+
133
+ 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.
144
+ for (const m of html.matchAll(/<img[^>]+src=["']([^"']+\.(?:png|jpe?g))["']/gi)) {
145
+ const src = m[1].replace(/^\//, '');
146
+ const noExt = src.replace(/\.(?:png|jpe?g)$/i, '');
147
+ if (webpSet.has(noExt)) pngWhereWebp.push(`${rel} → ${m[1]}`);
148
+ }
149
+
150
+ if (PLACEHOLDER_RE.test(html)) placeholderText.push(rel);
151
+ }
152
+
153
+ function reportList(name, list, severity = 'fail') {
154
+ if (list.length === 0) {
155
+ pass(name);
156
+ return;
157
+ }
158
+ const detail =
159
+ list.length <= 5
160
+ ? list.join(', ')
161
+ : `${list.slice(0, 5).join(', ')} … (+${list.length - 5} more)`;
162
+ (severity === 'fail' ? fail : warn)(name, detail);
163
+ }
164
+
165
+ reportList('Every page has <title>', missingTitle);
166
+ reportList('Every page has <meta description>', missingDescription);
167
+ reportList('Every page has canonical link', missingCanonical);
168
+ reportList('Every page has og:image', missingOgImage, 'warn');
169
+ reportList('Every page has JSON-LD schema', missingJsonLd);
170
+ reportList('No <img> references raw PNG/JPG where WebP exists', pngWhereWebp);
171
+ reportList('No placeholder text in production HTML', placeholderText);
172
+
173
+ // ─── Output ──────────────────────────────────────────────────────────────
174
+
175
+ printAndExit();
176
+
177
+ function printAndExit() {
178
+ const passes = checks.filter((c) => c.status === 'pass').length;
179
+ const warns = checks.filter((c) => c.status === 'warn').length;
180
+ const fails = checks.filter((c) => c.status === 'fail').length;
181
+
182
+ for (const c of checks) {
183
+ const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '!' : '✗';
184
+ const line = `${icon} ${c.name}`;
185
+ console.log(c.detail ? `${line}\n ${c.detail}` : line);
186
+ }
187
+ console.log(`\n${passes} pass · ${warns} warn · ${fails} fail`);
188
+ process.exit(fails > 0 ? 1 : 0);
189
+ }
@@ -0,0 +1,55 @@
1
+ # Sitewide checklist — every Codejitsu site
2
+
3
+ Walk through this list after any non-trivial change. Programmatic checks are runnable via `npx codejitsu-check` (runs from the site repo root after `npm run build`).
4
+
5
+ The runner covers the easy stuff (file presence, meta tags, canonical, schema script tags, PNG-where-WebP-exists, placeholder text). The rest needs human + Claude judgment.
6
+
7
+ ## Build + deploy
8
+
9
+ - [ ] `npm run build` exits 0 with no warnings about missing pages, unresolved imports, or broken links.
10
+ - [ ] `dist/` contains static HTML for every expected route. No `.html` route is missing.
11
+ - [ ] `wrangler.toml` is present and points to `dist`.
12
+ - [ ] `.github/workflows/daily-deploy.yml` exists; the `CLOUDFLARE_DEPLOY_HOOK_URL` secret is set in the repo (skip if site has no scheduled content).
13
+
14
+ ## URLs + routing
15
+
16
+ - [ ] Astro config has `trailingSlash: 'always'` and `output: 'static'`.
17
+ - [ ] No internal link in any `.astro`/`.tsx` file points to a URL without a trailing slash.
18
+ - [ ] Canonical URL appears on every page in `<head>` and matches the trailing-slash policy.
19
+
20
+ ## SEO
21
+
22
+ - [ ] Every page has `<title>` and `<meta name="description">`. No duplicates across pages.
23
+ - [ ] OG meta on every page: `og:title`, `og:description`, `og:image`, `og:url`, `og:type`.
24
+ - [ ] Twitter card meta on every page.
25
+ - [ ] JSON-LD schema appropriate per page type (Organization, LocalBusiness, BlogPosting, FAQPage). Use `@ibalzam/codejitsu-core/seo/schema` builders.
26
+ - [ ] `sitemap-index.xml` generated and lists every public page. Future-dated blog posts are excluded.
27
+ - [ ] `robots.txt` at root and references the sitemap.
28
+ - [ ] `llms.txt` and `llms-full.txt` at root, regenerated this build.
29
+
30
+ ## Images
31
+
32
+ - [ ] No `<img src="*.png">` or `<img src="*.jpg">` references in built HTML where a WebP equivalent exists.
33
+ - [ ] Astro `image.defaults` includes `format: 'webp'`.
34
+ - [ ] All images in `public/images/` over 500KB have been run through `codejitsu-optimize-images` or have a documented exception.
35
+
36
+ ## Performance
37
+
38
+ - [ ] No client-side JS on pages that don't need it (Astro should ship zero JS by default).
39
+ - [ ] `inlineStylesheets: 'always'` in build config (or justified opt-out).
40
+ - [ ] Hero image uses `loading="eager"` + `fetchpriority="high"`; everything else lazy-loaded.
41
+
42
+ ## Accessibility
43
+
44
+ - [ ] Every `<img>` has alt text (decorative images: `alt=""`).
45
+ - [ ] Every interactive element is keyboard-reachable and has a visible focus style.
46
+ - [ ] Color contrast meets WCAG AA on body text.
47
+
48
+ ## Content
49
+
50
+ - [ ] No placeholder text (`Lorem ipsum`, `TODO`, `FIXME`) in shipped routes.
51
+ - [ ] Every blog post in `content/blog/` has required frontmatter (see `modules/blog/checklist.md`).
52
+
53
+ ## When something on this list changes
54
+
55
+ If a new invariant is added to a Codejitsu site, add it here AND bump the package version with a `MIGRATIONS/<version>.md` note so existing sites can adopt it.
@@ -0,0 +1,87 @@
1
+ # Blog module — instructions for Claude
2
+
3
+ When the user asks to **implement codejitsu/core/blog** (or "add the blog system"), do the following.
4
+
5
+ ## What this module provides
6
+
7
+ A markdown-based blog with:
8
+ - File-based posts in `content/blog/*.md` (gray-matter frontmatter)
9
+ - Scheduled publishing — future-dated posts are hidden from public pages and sitemap, but their slugs stay buildable so OG meta scrapers (Hootsuite, etc.) can hit them
10
+ - Dual-slug resolution — filename slug (`2026-02-08-foo`) and canonical frontmatter slug (`foo`) both resolve to the same post; the frontmatter slug is canonical for SEO
11
+ - Reading time, tags, FAQs, categories
12
+ - Listing, tag, category pages
13
+
14
+ ## Wiring it into a site
15
+
16
+ ### 1. Install peer deps in the site (one-time)
17
+
18
+ ```bash
19
+ npm install gray-matter reading-time
20
+ ```
21
+ (They're transitive deps of `@ibalzam/codejitsu-core`, but Astro's bundler resolves them from the site's `node_modules`.)
22
+
23
+ ### 2. Create the site's blog instance
24
+
25
+ Copy `templates/lib/blog.ts` → `src/lib/blog.ts` in the site. Edit the config to set the site's default author and (optionally) its category list. The whole file is ~10 lines.
26
+
27
+ ### 3. Add page routes
28
+
29
+ Copy these from `templates/pages/` → `src/pages/` in the site:
30
+ - `blog/index.astro` — listing
31
+ - `blog/[...slug].astro` — detail (handles both filename and canonical slug forms)
32
+ - `blog/tag/[tag].astro` — tag pages
33
+ - `blog/category/[category].astro` — category pages (skip if site has no categories)
34
+
35
+ Adapt the markup to the site's design system. The page logic (data fetching, getStaticPaths) is the part that must stay correct — styling is the site's job.
36
+
37
+ ### 4. Add the first post
38
+
39
+ Copy `templates/content/_sample-post.md` → `content/blog/<today>-<slug>.md` and edit.
40
+
41
+ ### 5. Wire scheduled-post filter into the sitemap
42
+
43
+ In `astro.config.mjs`, import the site's blog instance and exclude future-dated slugs from the sitemap:
44
+
45
+ ```ts
46
+ import { blog } from './src/lib/blog';
47
+ const futureSlugs = await blog.getFutureBlogSlugs();
48
+
49
+ // in sitemap integration:
50
+ filter: (page) => {
51
+ const m = page.match(/\/blog\/([^/]+)\/?$/);
52
+ return !(m && futureSlugs.includes(m[1]));
53
+ }
54
+ ```
55
+
56
+ ### 6. Wire the daily-deploy GH Action
57
+
58
+ See `modules/deploy/CLAUDE.md`. The cron rebuilds the site so scheduled posts graduate from hidden to public on their publish date.
59
+
60
+ ## Post frontmatter shape
61
+
62
+ ```yaml
63
+ ---
64
+ title: "How to size a furnace" # required
65
+ description: "Quick guide to BTU sizing" # required (used as meta description)
66
+ date: 2026-03-15 # required; future date = hidden until that day
67
+ slug: how-to-size-a-furnace # optional; if set, this is the canonical URL
68
+ author: "Pearl Remodeling" # optional; falls back to defaultAuthor in config
69
+ image: /images/blog/furnace-sizing.webp # optional; used for OG + listing card
70
+ tags: [HVAC, Heating, Guides] # optional
71
+ faqs: # optional; rendered as FAQ schema + section
72
+ - question: "What BTU do I need?"
73
+ answer: "Roughly 30 BTU per sq ft as a starting point..."
74
+ ---
75
+ ```
76
+
77
+ ## What must NOT be done
78
+
79
+ - **Don't reimplement the loader.** Always import from `@ibalzam/codejitsu-core/blog` via the site's `src/lib/blog.ts`.
80
+ - **Don't bypass `getAllPosts()` for the listing.** It's already filtering future-dated posts; bypassing means drafts leak.
81
+ - **Don't add `getStaticPaths` that calls `getAllPosts()` alone for the detail page** — it'll exclude future-dated posts and break OG scraping for scheduled releases. Use `getAllPostSlugs()` for path generation.
82
+ - **Don't put `.mdx` files in `content/blog/`.** The loader is `.md` only. If MDX support is needed, raise it as a feature request — it's a deliberate scope decision.
83
+ - **Don't change the dual-slug resolution.** Old date-prefixed URLs must keep working alongside short canonical slugs.
84
+
85
+ ## Verify
86
+
87
+ Run `modules/blog/checklist.md` after wiring. Run `checklist/core.md` (sitewide).
@@ -0,0 +1,36 @@
1
+ # Blog module — checklist
2
+
3
+ ## Setup
4
+
5
+ - [ ] `src/lib/blog.ts` exists and calls `createBlog(...)` from `@ibalzam/codejitsu-core/blog`.
6
+ - [ ] `content/blog/` directory exists; at least one `.md` post is present.
7
+ - [ ] Page routes exist: `src/pages/blog/index.astro`, `src/pages/blog/[...slug].astro`.
8
+ - [ ] If site uses tags: `src/pages/blog/tag/[tag].astro` exists.
9
+ - [ ] If site uses categories: `src/pages/blog/category/[category].astro` exists and `categories` is passed to `createBlog`.
10
+
11
+ ## Post frontmatter
12
+
13
+ For every post in `content/blog/`:
14
+ - [ ] `title`, `description`, `date` are present.
15
+ - [ ] `description` is < 160 chars (meta description budget).
16
+ - [ ] `date` is ISO `YYYY-MM-DD` format.
17
+ - [ ] If `image` is set, the file exists in `public/`.
18
+ - [ ] If `faqs` is set, every entry has both `question` and `answer`.
19
+
20
+ ## Build behaviour
21
+
22
+ - [ ] Future-dated posts are absent from `dist/blog/index.html` (listing).
23
+ - [ ] Future-dated posts ARE built at `dist/blog/<slug>/index.html` (so OG scrapers can reach them).
24
+ - [ ] `sitemap-index.xml` does NOT contain URLs for future-dated posts.
25
+
26
+ ## SEO
27
+
28
+ - [ ] Each blog post page has `BlogPosting` JSON-LD schema (use `@ibalzam/codejitsu-core/seo/schema`).
29
+ - [ ] If post has `faqs`, the page also has `FAQPage` JSON-LD.
30
+ - [ ] Post pages have OG image set to `frontmatter.image` (absolute URL).
31
+ - [ ] Canonical URL on a post points to the canonical (frontmatter) slug, not the filename slug.
32
+
33
+ ## Daily deploy
34
+
35
+ - [ ] `.github/workflows/daily-deploy.yml` is present (see `modules/deploy/`).
36
+ - [ ] `CLOUDFLARE_DEPLOY_HOOK_URL` secret is configured in the repo.
@@ -0,0 +1 @@
1
+ export type { BlogPost, BlogPostMetadata, BlogCategory, FAQItem } from '../index.js';