@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.
@@ -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/schema`** typed JSON-LD builders: `organization()`, `localBusiness()`, `website()`, `blogPosting()`, `faqPage()`, `breadcrumbList()`, `service()`. Plus `jsonLd(obj)` for safe stringification.
10
- 2. **`@ibalzam/codejitsu-core/seo/sitemap`** helpers for `@astrojs/sitemap`: `defaultPriorityRules()`, `excludeFuturePosts()`, `composeFilters()`, `excludePatterns()`.
11
- 3. **`templates/Head.astro`** reusable head component that takes `{ title, description, canonical, ogImage, ogType, schema }` and injects everything.
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. Copy the Head component
18
+ ### 1. Create a thin site wrapper around Head
16
19
 
17
- `templates/Head.astro` `src/components/Head.astro` in the site. This is the single place that renders title, meta, OG, Twitter, canonical, and JSON-LD. Every layout `<head>` should include `<Head ... />`.
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 Head from '~/components/Head.astro';
24
- import { organization, blogPosting } from '@ibalzam/codejitsu-core/seo/schema';
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
- <Head
31
- title="Page title — Acme Co."
69
+ <SiteHead
70
+ title="Page title"
32
71
  description="..."
33
- schema={[organization(SITE)]}
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 { defaultPriorityRules, excludeFuturePosts, composeFilters, excludePatterns } from '@ibalzam/codejitsu-core/seo/sitemap';
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\//]), // site-specific exclusions
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 to point to the site's actual sitemap URL.
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
- A single `<Head schema={[a, b, c]} />` accepts multiple schemas. Each renders as its own `<script type="application/ld+json">`.
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 hand-write the canonical URL.** Use `Head`'s default (it builds from `Astro.url.pathname` + `Astro.site`) unless overriding for a specific reason.
88
- - **Don't `JSON.stringify` schemas yourself.** Use `jsonLd()` it escapes `</` so the script tag can't be broken out of.
89
- - **Don't reference relative URLs in `og:image`.** OG scrapers require absolute URLs. `Head` resolves this automatically when you pass a relative path.
90
- - **Don't add `og:image` to a page without an actual image at that path.** Empty OG images render as blank cards on socials.
91
- - **Don't omit the canonical tag on alternative-URL pages** (filename slug + canonical slug both exist for blog posts). The canonical must point to the frontmatter slug version.
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
- import { jsonLd } from '@ibalzam/codejitsu-core/seo/schema';
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
- /** Absolute URL (with trailing slash). Defaults to current page. */
8
- canonical?: string;
9
- /** Absolute URL to OG image. */
10
- ogImage?: string;
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
- ogType?: 'website' | 'article';
13
- /** JSON-LD schema objects to inject. Use builders from @ibalzam/codejitsu-core/seo/schema. */
14
- schema?: unknown[];
15
- /** Override <html lang>. Defaults to 'en'. */
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
- canonical,
23
- ogImage,
24
- ogType = 'website',
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 canonicalUrl = canonical ?? new URL(Astro.url.pathname, Astro.site).toString();
30
- const absOgImage = ogImage
31
- ? new URL(ogImage, Astro.site).toString()
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>{title}</title>
86
+ <title>{fullTitle}</title>
36
87
  <meta name="description" content={description} />
37
- <link rel="canonical" href={canonicalUrl} />
38
- {noindex && <meta name="robots" content="noindex,follow" />}
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
- <meta property="og:title" content={title} />
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={canonicalUrl} />
43
- <meta property="og:type" content={ogType} />
44
- {absOgImage && <meta property="og:image" content={absOgImage} />}
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={title} />
116
+ <meta name="twitter:title" content={fullTitle} />
48
117
  <meta name="twitter:description" content={description} />
49
- {absOgImage && <meta name="twitter:image" content={absOgImage} />}
118
+ {ogImage && <meta name="twitter:image" content={ogImage} />}
119
+ {ogImage && <meta name="twitter:image:alt" content={`${title} - ${siteName}`} />}
50
120
 
51
- {schema.map((item) => (
52
- <script type="application/ld+json" set:html={jsonLd(item)} />
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.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Shared core for Codejitsu Astro sites — reusable code and Claude-facing instructions for blog, SEO, images, deploy, and llms.txt.",
6
6
  "keywords": [
@@ -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.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
- };