@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/modules/seo/CLAUDE.md
CHANGED
|
@@ -6,31 +6,70 @@ When the user asks to **set up codejitsu/core/seo** (or "wire up SEO", "add sche
|
|
|
6
6
|
|
|
7
7
|
Three layers:
|
|
8
8
|
|
|
9
|
-
1. **`@ibalzam/codejitsu-core/seo
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
1. **`@ibalzam/codejitsu-core/seo`** (top-level) re-exports:
|
|
10
|
+
- **Schema builders:** `organization()`, `localBusiness()`, `website()`, `blogPosting()`, `faqPage()`, `breadcrumbList()`, `service()`
|
|
11
|
+
- **Safe injection:** `jsonLd(obj)` — stringifies + escapes `</` so the script tag can't be broken out of
|
|
12
|
+
- **Sitemap helpers:** `defaultPriorityRules()`, `excludeFuturePosts()`, `composeFilters()`, `excludePatterns()`
|
|
13
|
+
2. **`templates/Head.astro`** — reusable head component (rich: noindex, heroImage preload, article metadata, hreflang, OG/Twitter, JSON-LD).
|
|
14
|
+
3. **`templates/robots.txt`** — starter robots.
|
|
12
15
|
|
|
13
16
|
## Wiring it into a site
|
|
14
17
|
|
|
15
|
-
### 1.
|
|
18
|
+
### 1. Create a thin site wrapper around Head
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
Sites have site-specific values (analytics scripts, default OG image, brand name) that shouldn't pollute every page's import. Create `src/components/SiteHead.astro` wrapping the package's Head:
|
|
21
|
+
|
|
22
|
+
```astro
|
|
23
|
+
---
|
|
24
|
+
import Head from '@ibalzam/codejitsu-core/seo/Head.astro';
|
|
25
|
+
import config from '../../codejitsu.config';
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
image?: string;
|
|
32
|
+
type?: 'website' | 'article';
|
|
33
|
+
schema?: Record<string, unknown> | Record<string, unknown>[];
|
|
34
|
+
noindex?: boolean;
|
|
35
|
+
heroImage?: string;
|
|
36
|
+
author?: string;
|
|
37
|
+
publishedTime?: string;
|
|
38
|
+
modifiedTime?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const props = Astro.props;
|
|
42
|
+
---
|
|
43
|
+
<Head
|
|
44
|
+
{...props}
|
|
45
|
+
siteUrl={config.site.url}
|
|
46
|
+
siteName={config.site.name}
|
|
47
|
+
locale={config.site.locale ?? 'en_US'}
|
|
48
|
+
image={props.image ?? config.site.defaultOgImage}
|
|
49
|
+
titleSuffix={config.site.titleSuffix}
|
|
50
|
+
>
|
|
51
|
+
<slot />
|
|
52
|
+
</Head>
|
|
53
|
+
|
|
54
|
+
<!-- Site-specific analytics / tracking go here -->
|
|
55
|
+
<!-- e.g. <script is:inline async src="https://analytics.example.com/script.js" /> -->
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The site's layout uses `<SiteHead ... />` (not the package's `<Head ... />` directly), so layout pages stay clean.
|
|
18
59
|
|
|
19
60
|
### 2. Use it on every page
|
|
20
61
|
|
|
21
62
|
```astro
|
|
22
63
|
---
|
|
23
|
-
import
|
|
24
|
-
import { organization, blogPosting } from '@ibalzam/codejitsu-core/seo
|
|
25
|
-
|
|
26
|
-
const SITE = { name: 'Acme Co.', url: 'https://acme.com' };
|
|
64
|
+
import SiteHead from '~/components/SiteHead.astro';
|
|
65
|
+
import { organization, blogPosting } from '@ibalzam/codejitsu-core/seo';
|
|
27
66
|
---
|
|
28
67
|
<html lang="en">
|
|
29
68
|
<head>
|
|
30
|
-
<
|
|
31
|
-
title="Page title
|
|
69
|
+
<SiteHead
|
|
70
|
+
title="Page title"
|
|
32
71
|
description="..."
|
|
33
|
-
schema={[organization(
|
|
72
|
+
schema={[organization({ name: 'Acme', url: 'https://acme.com' })]}
|
|
34
73
|
/>
|
|
35
74
|
</head>
|
|
36
75
|
<body>...</body>
|
|
@@ -43,7 +82,12 @@ In `astro.config.mjs`:
|
|
|
43
82
|
|
|
44
83
|
```ts
|
|
45
84
|
import sitemap from '@astrojs/sitemap';
|
|
46
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
defaultPriorityRules,
|
|
87
|
+
excludeFuturePosts,
|
|
88
|
+
composeFilters,
|
|
89
|
+
excludePatterns,
|
|
90
|
+
} from '@ibalzam/codejitsu-core/seo';
|
|
47
91
|
import { blog } from './src/lib/blog';
|
|
48
92
|
|
|
49
93
|
const SITE = 'https://acme.com';
|
|
@@ -55,7 +99,7 @@ export default defineConfig({
|
|
|
55
99
|
sitemap({
|
|
56
100
|
filter: composeFilters(
|
|
57
101
|
excludeFuturePosts(futureSlugs),
|
|
58
|
-
excludePatterns([/\/lp\//, /\/draft\//]),
|
|
102
|
+
excludePatterns([/\/lp\//, /\/draft\//]),
|
|
59
103
|
),
|
|
60
104
|
serialize: defaultPriorityRules(SITE),
|
|
61
105
|
}),
|
|
@@ -65,7 +109,7 @@ export default defineConfig({
|
|
|
65
109
|
|
|
66
110
|
### 4. Copy robots.txt
|
|
67
111
|
|
|
68
|
-
`templates/robots.txt` → `public/robots.txt`. Edit the `Sitemap:` line
|
|
112
|
+
`templates/robots.txt` → `public/robots.txt`. Edit the `Sitemap:` line.
|
|
69
113
|
|
|
70
114
|
### 5. Per-page schema cheatsheet
|
|
71
115
|
|
|
@@ -79,16 +123,16 @@ export default defineConfig({
|
|
|
79
123
|
| Service area page | `localBusiness()` with `areaServed` set + `breadcrumbList()` |
|
|
80
124
|
| FAQ-heavy landing page | `faqPage()` + page-type schema |
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
`<SiteHead schema={[a, b, c]} />` accepts multiple schemas. Each renders as its own `<script type="application/ld+json">`.
|
|
83
127
|
|
|
84
128
|
## What must NOT be done
|
|
85
129
|
|
|
86
130
|
- **Don't inline schema objects.** Always use the builders so types catch missing required fields (e.g. `BlogPosting` requires `publisher`).
|
|
87
|
-
- **Don't
|
|
88
|
-
- **Don't
|
|
89
|
-
- **Don't
|
|
90
|
-
- **Don't
|
|
91
|
-
- **Don't
|
|
131
|
+
- **Don't `JSON.stringify` schemas yourself.** Use `jsonLd()` — escapes `</` so the script tag can't be broken out of. If you see a site doing `set:html={JSON.stringify(s)}`, replace with `set:html={jsonLd(s)}`.
|
|
132
|
+
- **Don't reference relative URLs in `og:image`.** OG scrapers require absolute URLs. The package's Head resolves this automatically when you pass a relative path.
|
|
133
|
+
- **Don't add `og:image` to a page without an actual image at that path.** Empty OG images render as blank cards.
|
|
134
|
+
- **Don't override `canonicalURL` by hand** — pass `path` instead, and let the Head build it. Keeps trailing-slash policy consistent.
|
|
135
|
+
- **Don't put site-specific analytics inside the package's Head.** Site-specific things belong in `SiteHead.astro` (the site's wrapper) so the package stays generic.
|
|
92
136
|
|
|
93
137
|
## Verify
|
|
94
138
|
|
|
@@ -1,53 +1,125 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Codejitsu Head — drop into a layout's <head>.
|
|
4
|
+
*
|
|
5
|
+
* Wraps SEO meta, OG/Twitter, canonical, hreflang, article metadata,
|
|
6
|
+
* hero preload, and JSON-LD schema injection. Site-specific analytics
|
|
7
|
+
* (GA, Ahrefs, reCAPTCHA, etc.) belong in a site wrapper, not here.
|
|
8
|
+
*
|
|
9
|
+
* Recommended: create `src/components/SiteHead.astro` that imports this
|
|
10
|
+
* and passes defaults from your `codejitsu.config.ts`. See module CLAUDE.md.
|
|
11
|
+
*/
|
|
12
|
+
import { jsonLd } from '@ibalzam/codejitsu-core/seo';
|
|
3
13
|
|
|
4
14
|
interface Props {
|
|
15
|
+
/** Page title (will be combined with `titleSuffix` if not too long). */
|
|
5
16
|
title: string;
|
|
17
|
+
/** Page description (meta + og + twitter). */
|
|
6
18
|
description: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
|
|
19
|
+
/** Optional explicit path (without site URL). Defaults to current page path. */
|
|
20
|
+
path?: string;
|
|
21
|
+
/** OG image. Relative paths are resolved against site URL. */
|
|
22
|
+
image?: string;
|
|
11
23
|
/** og:type. Defaults to 'website'. */
|
|
12
|
-
|
|
13
|
-
/** JSON-LD schema
|
|
14
|
-
schema?: unknown[];
|
|
15
|
-
/**
|
|
24
|
+
type?: 'website' | 'article';
|
|
25
|
+
/** JSON-LD schema(s) to inject. Use builders from `@ibalzam/codejitsu-core/seo`. */
|
|
26
|
+
schema?: Record<string, unknown> | Record<string, unknown>[];
|
|
27
|
+
/** Emit noindex,nofollow. */
|
|
16
28
|
noindex?: boolean;
|
|
29
|
+
/** Hero image to preload (LCP). Absolute path or full URL. */
|
|
30
|
+
heroImage?: string;
|
|
31
|
+
/** Article author name (only emitted when type='article'). */
|
|
32
|
+
author?: string;
|
|
33
|
+
/** ISO date strings (only emitted when type='article'). */
|
|
34
|
+
publishedTime?: string;
|
|
35
|
+
modifiedTime?: string;
|
|
36
|
+
/** Site URL (no trailing slash). Required. e.g. 'https://example.com'. */
|
|
37
|
+
siteUrl: string;
|
|
38
|
+
/** Site brand name. Used for og:site_name and (if present) title suffix. */
|
|
39
|
+
siteName: string;
|
|
40
|
+
/** Locale, e.g. 'en_US'. Default 'en_US'. */
|
|
41
|
+
locale?: string;
|
|
42
|
+
/** Appended to <title> with " | "; only if combined length <= 65 chars. */
|
|
43
|
+
titleSuffix?: string;
|
|
17
44
|
}
|
|
18
45
|
|
|
19
46
|
const {
|
|
20
47
|
title,
|
|
21
48
|
description,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
schema
|
|
49
|
+
path,
|
|
50
|
+
image,
|
|
51
|
+
type = 'website',
|
|
52
|
+
schema,
|
|
26
53
|
noindex = false,
|
|
54
|
+
heroImage,
|
|
55
|
+
author,
|
|
56
|
+
publishedTime,
|
|
57
|
+
modifiedTime,
|
|
58
|
+
siteUrl,
|
|
59
|
+
siteName,
|
|
60
|
+
locale = 'en_US',
|
|
61
|
+
titleSuffix,
|
|
27
62
|
} = Astro.props;
|
|
28
63
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
64
|
+
const SITE = siteUrl.replace(/\/$/, '');
|
|
65
|
+
const currentPath = path ?? Astro.url.pathname;
|
|
66
|
+
const canonicalPath = currentPath
|
|
67
|
+
? `/${currentPath.replace(/^\//, '').replace(/\/$/, '')}/`
|
|
68
|
+
: '/';
|
|
69
|
+
const isHomepage = canonicalPath === '/';
|
|
70
|
+
const canonicalURL = `${SITE}${canonicalPath}`;
|
|
71
|
+
|
|
72
|
+
const suffix = titleSuffix ?? siteName;
|
|
73
|
+
const candidateTitle = `${title} | ${suffix}`;
|
|
74
|
+
const fullTitle = isHomepage || !suffix
|
|
75
|
+
? title
|
|
76
|
+
: candidateTitle.length <= 65 ? candidateTitle : title;
|
|
77
|
+
|
|
78
|
+
const ogImage = image
|
|
79
|
+
? (image.startsWith('http') ? image : `${SITE}${image.startsWith('/') ? '' : '/'}${image}`)
|
|
32
80
|
: undefined;
|
|
81
|
+
|
|
82
|
+
const schemas = schema ? (Array.isArray(schema) ? schema : [schema]) : [];
|
|
83
|
+
const hreflang = locale.replace('_', '-');
|
|
33
84
|
---
|
|
34
85
|
|
|
35
|
-
<title>{
|
|
86
|
+
<title>{fullTitle}</title>
|
|
36
87
|
<meta name="description" content={description} />
|
|
37
|
-
<link rel="canonical" href={
|
|
38
|
-
|
|
88
|
+
<link rel="canonical" href={canonicalURL} />
|
|
89
|
+
<link rel="alternate" href={canonicalURL} hreflang={hreflang} />
|
|
90
|
+
<link rel="alternate" href={canonicalURL} hreflang="x-default" />
|
|
91
|
+
|
|
92
|
+
{heroImage && (
|
|
93
|
+
<link rel="preload" as="image" href={heroImage} type="image/webp" fetchpriority="high" />
|
|
94
|
+
)}
|
|
39
95
|
|
|
40
|
-
|
|
96
|
+
{noindex
|
|
97
|
+
? <meta name="robots" content="noindex, nofollow" />
|
|
98
|
+
: <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
<meta property="og:type" content={type} />
|
|
102
|
+
<meta property="og:title" content={fullTitle} />
|
|
41
103
|
<meta property="og:description" content={description} />
|
|
42
|
-
<meta property="og:url" content={
|
|
43
|
-
<meta property="og:
|
|
44
|
-
|
|
104
|
+
<meta property="og:url" content={canonicalURL} />
|
|
105
|
+
{ogImage && <meta property="og:image" content={ogImage} />}
|
|
106
|
+
<meta property="og:site_name" content={siteName} />
|
|
107
|
+
<meta property="og:locale" content={locale} />
|
|
108
|
+
{ogImage && <meta property="og:image:width" content="1200" />}
|
|
109
|
+
{ogImage && <meta property="og:image:height" content="630" />}
|
|
110
|
+
|
|
111
|
+
{type === 'article' && publishedTime && <meta property="article:published_time" content={publishedTime} />}
|
|
112
|
+
{type === 'article' && modifiedTime && <meta property="article:modified_time" content={modifiedTime} />}
|
|
113
|
+
{type === 'article' && author && <meta property="article:author" content={author} />}
|
|
45
114
|
|
|
46
115
|
<meta name="twitter:card" content="summary_large_image" />
|
|
47
|
-
<meta name="twitter:title" content={
|
|
116
|
+
<meta name="twitter:title" content={fullTitle} />
|
|
48
117
|
<meta name="twitter:description" content={description} />
|
|
49
|
-
{
|
|
118
|
+
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
|
119
|
+
{ogImage && <meta name="twitter:image:alt" content={`${title} - ${siteName}`} />}
|
|
50
120
|
|
|
51
|
-
{
|
|
52
|
-
<script type="application/ld+json" set:html={jsonLd(
|
|
121
|
+
{schemas.map((s) => (
|
|
122
|
+
<script type="application/ld+json" is:inline set:html={jsonLd(s)} />
|
|
53
123
|
))}
|
|
124
|
+
|
|
125
|
+
<slot />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibalzam/codejitsu-core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": [
|
|
@@ -32,12 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"exports": {
|
|
34
34
|
".": "./src/index.ts",
|
|
35
|
+
"./config": "./modules/config/src/index.ts",
|
|
35
36
|
"./blog": "./modules/blog/src/index.ts",
|
|
36
37
|
"./blog/components": "./modules/blog/src/components/index.ts",
|
|
37
38
|
"./seo": "./modules/seo/src/index.ts",
|
|
38
39
|
"./seo/schema": "./modules/seo/src/schema.ts",
|
|
39
40
|
"./seo/sitemap": "./modules/seo/src/sitemap.ts",
|
|
40
41
|
"./images": "./modules/images/src/index.ts",
|
|
42
|
+
"./llms": "./modules/llms/src/generate.mjs",
|
|
41
43
|
"./package.json": "./package.json"
|
|
42
44
|
},
|
|
43
45
|
"bin": {
|
|
@@ -61,6 +63,14 @@
|
|
|
61
63
|
"peerDependencies": {
|
|
62
64
|
"astro": ">=5.0.0"
|
|
63
65
|
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"astro": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"optionalDependencies": {
|
|
72
|
+
"jiti": "^2.0.0"
|
|
73
|
+
},
|
|
64
74
|
"dependencies": {
|
|
65
75
|
"gray-matter": "^4.0.3",
|
|
66
76
|
"reading-time": "^1.5.0",
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CORE_VERSION = '0.
|
|
1
|
+
export const CORE_VERSION = '0.2.0';
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** @type {import('@ibalzam/codejitsu-core/images').OptimizeImagesConfig} */
|
|
2
|
-
export default {
|
|
3
|
-
sourceDir: 'public/images',
|
|
4
|
-
thumbDir: 'public/images/thumbs',
|
|
5
|
-
defaultQuality: 75,
|
|
6
|
-
defaultMaxSize: 1200,
|
|
7
|
-
thumbSize: 400,
|
|
8
|
-
thumbQuality: 70,
|
|
9
|
-
|
|
10
|
-
// Per-file overrides. Key = path relative to sourceDir, without extension.
|
|
11
|
-
specialRules: {
|
|
12
|
-
// Example: aggressively compress the logo, also generate AVIF.
|
|
13
|
-
// 'logos/logo': { maxWidth: 329, maxHeight: 70, quality: 35, generateAvif: true },
|
|
14
|
-
//
|
|
15
|
-
// Example: OG share image — keep quality high, optimize the PNG in place too.
|
|
16
|
-
// 'sharing/og-default': { maxWidth: 1200, maxHeight: 630, quality: 85, optimizePng: true },
|
|
17
|
-
},
|
|
18
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
siteUrl: 'https://example.com',
|
|
3
|
-
siteName: 'Example Co.',
|
|
4
|
-
tagline: 'What we do in one line',
|
|
5
|
-
|
|
6
|
-
about: `Short paragraph describing what the site is. Used at the top of llms.txt.`,
|
|
7
|
-
|
|
8
|
-
aboutFull: `Longer about content, used in llms-full.txt. Can include multiple paragraphs,
|
|
9
|
-
key differentiators, history, etc.`,
|
|
10
|
-
|
|
11
|
-
// Set to your blog directory to auto-include recent posts.
|
|
12
|
-
blogDir: 'content/blog',
|
|
13
|
-
blogLimit: 10,
|
|
14
|
-
blogFullLimit: 20,
|
|
15
|
-
|
|
16
|
-
sections: [
|
|
17
|
-
{
|
|
18
|
-
title: 'Services',
|
|
19
|
-
description: 'What we offer.',
|
|
20
|
-
items: [
|
|
21
|
-
// { title: 'Example service', description: 'One line.', url: '/services/example/' },
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
title: 'Key Pages',
|
|
26
|
-
items: [
|
|
27
|
-
{ title: 'About', description: 'About the company.', url: '/about/' },
|
|
28
|
-
{ title: 'Contact', description: 'Get in touch.', url: '/contact/' },
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
|
|
33
|
-
aiGuidance: `When referencing this company:
|
|
34
|
-
- We are <industry/type>
|
|
35
|
-
- Target audience: <who>
|
|
36
|
-
- Key differentiator: <what>
|
|
37
|
-
- Pricing: <if relevant>
|
|
38
|
-
- Contact: <how>`,
|
|
39
|
-
};
|