@ibalzam/codejitsu-core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codejitsu.mjs +23 -3
- package/modules/audit/src/a11y/runner.mjs +146 -0
- package/modules/audit/src/ai/runner.mjs +176 -0
- package/modules/audit/src/groups/ai-discoverability.mjs +51 -0
- package/modules/audit/src/groups/analytics.mjs +54 -0
- package/modules/audit/src/groups/blog-quality.mjs +98 -0
- package/modules/audit/src/groups/content.mjs +87 -0
- package/modules/audit/src/groups/forms.mjs +112 -0
- package/modules/audit/src/groups/links.mjs +58 -0
- package/modules/audit/src/groups/performance.mjs +117 -0
- package/modules/audit/src/groups/seo.mjs +178 -0
- package/modules/audit/src/groups/structure.mjs +105 -0
- package/modules/audit/src/http/runner.mjs +185 -0
- package/modules/audit/src/run.mjs +168 -0
- package/modules/audit/src/util.mjs +72 -0
- package/modules/config/src/types.d.ts +21 -0
- package/modules/config/src/types.ts +23 -0
- package/modules/llms/src/generate.mjs +22 -5
- package/modules/rehype/CLAUDE.md +64 -0
- package/modules/rehype/src/trailing-slash.mjs +88 -0
- package/package.json +2 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin that enforces a trailing-slash policy on internal `<a href>`
|
|
3
|
+
* values produced by markdown content. Astro's `trailingSlash: 'always'`
|
|
4
|
+
* controls page routing but does NOT rewrite hand-written hrefs in markdown
|
|
5
|
+
* or component templates. This plugin fills that gap for markdown content.
|
|
6
|
+
*
|
|
7
|
+
* Usage in astro.config.mjs:
|
|
8
|
+
*
|
|
9
|
+
* import trailingSlash from '@ibalzam/codejitsu-core/rehype/trailing-slash';
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* markdown: {
|
|
13
|
+
* rehypePlugins: [trailingSlash],
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Or with options:
|
|
18
|
+
*
|
|
19
|
+
* rehypePlugins: [[trailingSlash, { policy: 'always' }]]
|
|
20
|
+
*
|
|
21
|
+
* What it skips (leaves untouched):
|
|
22
|
+
* - External URLs (http://, https://, //, mailto:, tel:, etc.)
|
|
23
|
+
* - Anchor-only links (#section)
|
|
24
|
+
* - Paths ending in a file extension (.pdf, .html, .webp, ...)
|
|
25
|
+
* - Root path `/`
|
|
26
|
+
*
|
|
27
|
+
* @param {object} [opts]
|
|
28
|
+
* @param {'always' | 'never' | 'preserve'} [opts.policy='always']
|
|
29
|
+
*/
|
|
30
|
+
export default function rehypeTrailingSlash(opts = {}) {
|
|
31
|
+
const policy = opts.policy ?? 'always';
|
|
32
|
+
if (policy === 'preserve') return () => {};
|
|
33
|
+
|
|
34
|
+
return (tree) => {
|
|
35
|
+
walk(tree, (node) => {
|
|
36
|
+
if (node.tagName !== 'a') return;
|
|
37
|
+
const href = node.properties?.href;
|
|
38
|
+
if (typeof href !== 'string') return;
|
|
39
|
+
|
|
40
|
+
const normalized = normalize(href, policy);
|
|
41
|
+
if (normalized !== href) {
|
|
42
|
+
node.properties.href = normalized;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walk(node, fn) {
|
|
49
|
+
if (node?.type === 'element') fn(node);
|
|
50
|
+
if (Array.isArray(node?.children)) {
|
|
51
|
+
for (const child of node.children) walk(child, fn);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply policy to a single href. Pure, no side-effects — exported for tests.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} href
|
|
59
|
+
* @param {'always' | 'never'} policy
|
|
60
|
+
*/
|
|
61
|
+
export function normalize(href, policy) {
|
|
62
|
+
if (!href.startsWith('/')) return href; // external, anchor, relative — skip
|
|
63
|
+
if (href.startsWith('//')) return href; // protocol-relative external
|
|
64
|
+
if (href === '/') return href; // root is its own canonical
|
|
65
|
+
|
|
66
|
+
// Split path / query / fragment so we don't break /foo?bar=baz or /foo#anchor.
|
|
67
|
+
const m = href.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
|
|
68
|
+
if (!m) return href;
|
|
69
|
+
let path = m[1];
|
|
70
|
+
const query = m[2] ?? '';
|
|
71
|
+
const fragment = m[3] ?? '';
|
|
72
|
+
|
|
73
|
+
if (!path || path === '/') return href;
|
|
74
|
+
|
|
75
|
+
// Last segment with a `.` is likely a file (e.g. /robots.txt, /og-image.webp).
|
|
76
|
+
const lastSeg = path.split('/').filter(Boolean).pop() ?? '';
|
|
77
|
+
if (lastSeg.includes('.')) return href;
|
|
78
|
+
|
|
79
|
+
const endsWithSlash = path.endsWith('/');
|
|
80
|
+
|
|
81
|
+
if (policy === 'always' && !endsWithSlash) {
|
|
82
|
+
path = `${path}/`;
|
|
83
|
+
} else if (policy === 'never' && endsWithSlash) {
|
|
84
|
+
path = path.replace(/\/+$/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return `${path}${query}${fragment}`;
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibalzam/codejitsu-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": [
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"default": "./modules/seo/src/sitemap.js"
|
|
61
61
|
},
|
|
62
62
|
"./seo/Head.astro": "./modules/seo/templates/Head.astro",
|
|
63
|
+
"./rehype/trailing-slash": "./modules/rehype/src/trailing-slash.mjs",
|
|
63
64
|
"./images": {
|
|
64
65
|
"types": "./modules/images/src/index.d.ts",
|
|
65
66
|
"default": "./modules/images/src/index.js"
|