contentbit 0.2.0 → 0.3.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/README.md +7 -1
- package/dist/bin.js +766 -43
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +55 -9
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +47 -7
- package/dist/commands/links.d.ts +3 -0
- package/dist/commands/links.d.ts.map +1 -0
- package/dist/commands/links.js +97 -0
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +2 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +20 -1
- package/dist/link-options.d.ts +10 -0
- package/dist/link-options.d.ts.map +1 -0
- package/dist/link-options.js +31 -0
- package/dist/links-io.d.ts +3 -0
- package/dist/links-io.d.ts.map +1 -0
- package/dist/links-io.js +14 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +766 -43
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAyMnC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,+EAA+E;AAC/E,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB3E"}
|
package/dist/commands/agents.js
CHANGED
|
@@ -7,7 +7,7 @@ import { parseArgs } from 'node:util';
|
|
|
7
7
|
// registry stays the single source of truth and nothing can drift. Bump the
|
|
8
8
|
// frontmatter version when a template changes; `contentbit agents` re-runs
|
|
9
9
|
// overwrite in place.
|
|
10
|
-
const TEMPLATE_VERSION =
|
|
10
|
+
const TEMPLATE_VERSION = 2;
|
|
11
11
|
const AUTHOR_SKILL = `---
|
|
12
12
|
name: contentbit-author
|
|
13
13
|
description: |
|
|
@@ -30,6 +30,8 @@ Check \`package.json\` for a \`content:check\` script. It holds the canonical
|
|
|
30
30
|
validate invocation for this project: the content glob and, if present, the
|
|
31
31
|
\`--registry <path>\` flag pointing at custom block definitions. Reuse both
|
|
32
32
|
below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
33
|
+
If the project has a \`content:links\` script, use it for the internal-link
|
|
34
|
+
index; otherwise run \`contentbit links <content glob>\` directly.
|
|
33
35
|
|
|
34
36
|
## The loop
|
|
35
37
|
|
|
@@ -44,7 +46,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
|
44
46
|
|
|
45
47
|
2. **Write the document.** Plain Markdown everywhere; blocks only where the
|
|
46
48
|
guide's use-when guidance fits. Keep frontmatter consistent with sibling
|
|
47
|
-
documents in the same folder.
|
|
49
|
+
documents in the same folder. If sibling documents use \`slug\`, \`linksTo\`,
|
|
50
|
+
\`aliases\`, or \`keywords\`, run the link index first:
|
|
51
|
+
|
|
52
|
+
\`\`\`sh
|
|
53
|
+
contentbit links <content glob>
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
Read \`.contentbit/link-index.json\` to pick existing slugs and related
|
|
57
|
+
pages. Author only \`slug\`, \`linksTo\`, \`aliases\`, and \`keywords\` in
|
|
58
|
+
frontmatter; never write derived \`linkedFrom\` into source files. When
|
|
59
|
+
creating a linked page, include \`keywords.primary\` and
|
|
60
|
+
\`keywords.secondary\` with search-intent phrases that would help future
|
|
61
|
+
agents choose this page as a \`linksTo\` target.
|
|
48
62
|
|
|
49
63
|
3. **Validate and fix until clean:**
|
|
50
64
|
|
|
@@ -54,8 +68,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
|
54
68
|
|
|
55
69
|
Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
|
|
56
70
|
with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
|
|
57
|
-
errors remain.
|
|
58
|
-
|
|
71
|
+
errors remain. If the document has link frontmatter, validate the full
|
|
72
|
+
content glob so cross-file links are checked against the whole graph. Fix
|
|
73
|
+
every diagnostic and re-run. Never finish with a failing validate.
|
|
74
|
+
|
|
75
|
+
4. **Refresh internal links when present:**
|
|
76
|
+
|
|
77
|
+
\`\`\`sh
|
|
78
|
+
contentbit links <content glob> --fix
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
\`--fix\` only rewrites \`linksTo\` values that point at known aliases. It
|
|
82
|
+
does not invent links, remove aliases, or write backlinks. Re-run validate
|
|
83
|
+
after it changes files.
|
|
59
84
|
|
|
60
85
|
## Failure modes
|
|
61
86
|
|
|
@@ -77,6 +102,8 @@ version: ${TEMPLATE_VERSION}
|
|
|
77
102
|
|
|
78
103
|
\`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
|
|
79
104
|
tool: it always exits 0, even when documents have validation errors.
|
|
105
|
+
\`contentbit links\` builds the frontmatter-authored internal-link graph and
|
|
106
|
+
prints link diagnostics.
|
|
80
107
|
|
|
81
108
|
## Gather
|
|
82
109
|
|
|
@@ -85,23 +112,31 @@ content glob and \`--registry\` flag, then:
|
|
|
85
112
|
|
|
86
113
|
\`\`\`sh
|
|
87
114
|
contentbit stats "content/**/*.md" [--registry <path>]
|
|
115
|
+
contentbit links "content/**/*.md"
|
|
88
116
|
\`\`\`
|
|
89
117
|
|
|
90
118
|
One matched file prints a single stats object; multiple files print an array.
|
|
91
119
|
Each entry includes the file path, frontmatter data, a heading \`outline\` with
|
|
92
120
|
per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
|
|
93
121
|
a \`validation\` summary (\`errors\`/\`warnings\`).
|
|
122
|
+
\`contentbit links\` also writes \`.contentbit/link-index.json\`, whose pages
|
|
123
|
+
contain \`slug\`, resolved \`linksTo\`, derived \`linkedFrom\`, \`aliases\`, and
|
|
124
|
+
\`keywords\`.
|
|
94
125
|
|
|
95
126
|
## Interpret
|
|
96
127
|
|
|
97
128
|
Prioritize findings in this order:
|
|
98
129
|
|
|
99
130
|
1. **Validation errors and warnings** — broken content ships broken pages.
|
|
100
|
-
2. **
|
|
101
|
-
|
|
131
|
+
2. **Internal-link errors** — unresolved links, duplicate slugs, and alias
|
|
132
|
+
conflicts from \`contentbit links\`.
|
|
133
|
+
3. **Orphans and self-links** — link warnings that point to isolated or noisy
|
|
134
|
+
pages.
|
|
135
|
+
4. **Thin documents** — outline sections with very low word counts.
|
|
136
|
+
5. **Block-less documents** — \`blocks.byName\` empty where sibling documents
|
|
102
137
|
use blocks; structure (steps, callouts, comparisons, faq) may be missing.
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
6. **Missing or inconsistent frontmatter** compared to sibling documents.
|
|
139
|
+
7. **Structural imbalance** — skipped heading levels, single-section walls of text.
|
|
105
140
|
|
|
106
141
|
## Report
|
|
107
142
|
|
|
@@ -117,14 +152,22 @@ This project validates Markdown content with contentbit. Documents are plain
|
|
|
117
152
|
Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
|
|
118
153
|
The \`content:check\` script in package.json holds the canonical validate
|
|
119
154
|
command — the content glob and the \`--registry\` flag — reuse its arguments.
|
|
155
|
+
If the project has a \`content:links\` script, use it to build the internal-link
|
|
156
|
+
index; otherwise run \`contentbit links <content glob>\`.
|
|
120
157
|
|
|
121
158
|
When writing or editing content:
|
|
122
159
|
|
|
123
160
|
1. Fetch the live authoring guide first — never guess block syntax:
|
|
124
161
|
\`contentbit instructions --audience llm [--registry <path>]\`
|
|
125
162
|
2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
|
|
126
|
-
3.
|
|
163
|
+
3. If sibling documents use \`slug\` / \`linksTo\`, read
|
|
164
|
+
\`.contentbit/link-index.json\` from \`contentbit links <content glob>\` and
|
|
165
|
+
author frontmatter links with existing slugs. When creating a linked page,
|
|
166
|
+
include \`keywords.primary\` and \`keywords.secondary\` with search-intent
|
|
167
|
+
phrases future agents can use to choose related pages.
|
|
168
|
+
4. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
|
|
127
169
|
Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
|
|
170
|
+
For link frontmatter, validate the full content glob so cross-file checks run.
|
|
128
171
|
|
|
129
172
|
When auditing content health:
|
|
130
173
|
|
|
@@ -132,6 +175,9 @@ When auditing content health:
|
|
|
132
175
|
and always exits 0: outline word counts, block usage, link domains, and
|
|
133
176
|
validation error/warning counts. Flag validation issues, thin documents, and
|
|
134
177
|
block-less pages first.
|
|
178
|
+
- \`contentbit links "content/**/*.md" [--fix]\` builds
|
|
179
|
+
\`.contentbit/link-index.json\`, reports dangling links/orphans, and rewrites
|
|
180
|
+
alias references in \`linksTo\` when \`--fix\` is used.
|
|
135
181
|
|
|
136
182
|
If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
|
|
137
183
|
of inventing block syntax.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAianC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqOzE"}
|
package/dist/commands/init.js
CHANGED
|
@@ -78,7 +78,18 @@ export const blockComponents: Record<string, BlockComponent> = {
|
|
|
78
78
|
}
|
|
79
79
|
`;
|
|
80
80
|
}
|
|
81
|
-
const EXAMPLE_CONTENT =
|
|
81
|
+
const EXAMPLE_CONTENT = `---
|
|
82
|
+
slug: hello-content-blocks
|
|
83
|
+
linksTo:
|
|
84
|
+
- related-contentbit-workflows
|
|
85
|
+
aliases:
|
|
86
|
+
- getting-started-contentbit
|
|
87
|
+
keywords:
|
|
88
|
+
primary: validated Markdown blocks
|
|
89
|
+
secondary: [content workflow, agent writing]
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Hello, Content Blocks
|
|
82
93
|
|
|
83
94
|
Regular Markdown works everywhere. Blocks add validated structure:
|
|
84
95
|
|
|
@@ -100,6 +111,27 @@ The Analytical Engine weaves algebraic patterns just as the Jacquard loom
|
|
|
100
111
|
weaves flowers and leaves.
|
|
101
112
|
:::
|
|
102
113
|
`;
|
|
114
|
+
const RELATED_CONTENT = `---
|
|
115
|
+
slug: related-contentbit-workflows
|
|
116
|
+
linksTo:
|
|
117
|
+
- hello-content-blocks
|
|
118
|
+
keywords:
|
|
119
|
+
primary: contentbit workflow
|
|
120
|
+
secondary: [validation loop, internal links]
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# Related contentbit workflows
|
|
124
|
+
|
|
125
|
+
This supporting page exists to show internal links in frontmatter. The link
|
|
126
|
+
graph is authored once with \`slug\` and \`linksTo\`, then contentbit derives
|
|
127
|
+
\`linkedFrom\` in \`.contentbit/link-index.json\`.
|
|
128
|
+
|
|
129
|
+
:::callout{type="note"}
|
|
130
|
+
Run \`contentbit links "content/**/*.md" --fix\` after renaming a page. Alias
|
|
131
|
+
references in \`linksTo\` are rewritten to the current slug, while \`aliases\`
|
|
132
|
+
stays as the rename record.
|
|
133
|
+
:::
|
|
134
|
+
`;
|
|
103
135
|
/** The wrapper component: styled pack or headless, with or without a Markdown lib. */
|
|
104
136
|
function reactComponent(styled, mdWired, blocksImport) {
|
|
105
137
|
const mdImport = mdWired ? "import ReactMarkdown from 'react-markdown'\n" : '';
|
|
@@ -117,7 +149,7 @@ import { ContentRenderer } from '@/components/content-blocks/content-renderer'`
|
|
|
117
149
|
return `'use client'
|
|
118
150
|
|
|
119
151
|
import { genericBlocks } from '@contentbit/blocks'
|
|
120
|
-
import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
|
|
152
|
+
import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
|
|
121
153
|
${reactImport}${mdImport}${rendererImport}
|
|
122
154
|
// Everything block-related lives in the blocks/ folder: definitions in
|
|
123
155
|
// registry.ts (shared with the validate CLI), components in components.tsx.
|
|
@@ -127,7 +159,7 @@ import { blockComponents } from '${blocksImport}/components'
|
|
|
127
159
|
const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
|
|
128
160
|
|
|
129
161
|
export function Content({ source }: { source: string }) {
|
|
130
|
-
const result = validateDocument(parseDocument(source), registry)
|
|
162
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
|
|
131
163
|
return (
|
|
132
164
|
<${renderer}
|
|
133
165
|
document={result.document}
|
|
@@ -151,14 +183,14 @@ const renderMarkdown = (md) => mdIt.render(md)`
|
|
|
151
183
|
const renderMarkdown = undefined`;
|
|
152
184
|
return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
|
|
153
185
|
import { genericBlocks } from '@contentbit/blocks'
|
|
154
|
-
import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
|
|
186
|
+
import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
|
|
155
187
|
import { renderToHtml } from '@contentbit/html'
|
|
156
188
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
157
189
|
${wiring}
|
|
158
190
|
|
|
159
191
|
const source = await readFile('content/example.md', 'utf8')
|
|
160
192
|
const registry = createBlockRegistry().use(genericBlocks())
|
|
161
|
-
const result = validateDocument(parseDocument(source), registry)
|
|
193
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
|
|
162
194
|
const html = renderToHtml(result.document, { renderMarkdown })
|
|
163
195
|
await writeFile('example.html', html, 'utf8')
|
|
164
196
|
console.log('wrote example.html')
|
|
@@ -464,6 +496,7 @@ export async function initCommand(args, io) {
|
|
|
464
496
|
const files = [
|
|
465
497
|
['blocks/registry.ts', REGISTRY_TEMPLATE],
|
|
466
498
|
['content/example.md', EXAMPLE_CONTENT],
|
|
499
|
+
['content/related.md', RELATED_CONTENT],
|
|
467
500
|
];
|
|
468
501
|
const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
|
|
469
502
|
// shadcn project? Pull the styled component pack from the contentbit registry.
|
|
@@ -520,15 +553,21 @@ export async function initCommand(args, io) {
|
|
|
520
553
|
const result = await scaffold(join(cwd, rel), content);
|
|
521
554
|
io.stdout(`${result}: ${rel}`);
|
|
522
555
|
}
|
|
523
|
-
// Wire
|
|
556
|
+
// Wire content scripts.
|
|
524
557
|
const fresh = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
525
558
|
fresh.scripts ??= {};
|
|
526
559
|
if (!fresh.scripts['content:check']) {
|
|
527
560
|
fresh.scripts['content:check'] =
|
|
528
561
|
'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
|
|
529
|
-
await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}\n`, 'utf8');
|
|
530
562
|
io.stdout('added script: content:check');
|
|
531
563
|
}
|
|
564
|
+
if (!fresh.scripts['content:links']) {
|
|
565
|
+
fresh.scripts['content:links'] = 'contentbit links "content/**/*.md"';
|
|
566
|
+
io.stdout('added script: content:links');
|
|
567
|
+
}
|
|
568
|
+
if (!pkg.scripts?.['content:check'] || !pkg.scripts?.['content:links']) {
|
|
569
|
+
await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}\n`, 'utf8');
|
|
570
|
+
}
|
|
532
571
|
// Generate the LLM authoring guide from the registry, ready to paste into a prompt.
|
|
533
572
|
let registry;
|
|
534
573
|
try {
|
|
@@ -551,6 +590,7 @@ export async function initCommand(args, io) {
|
|
|
551
590
|
io.stdout('');
|
|
552
591
|
io.stdout('Done. Next steps:');
|
|
553
592
|
io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
|
|
593
|
+
io.stdout(` Build the link index: ${detectPackageManager(cwd)} run content:links`);
|
|
554
594
|
if (target === 'react') {
|
|
555
595
|
if (!values['no-page'] && layout.pagePath) {
|
|
556
596
|
io.stdout(' 2. Start the dev server and open /example to see the article rendered.');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/commands/links.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAKnC,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF1E"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { aliasReplacementsForPage, buildLinkIndex, extractFrontmatter, formatDiagnostic, serializeLinkIndex, validateLinks, } from '@contentbit/core';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { glob } from 'tinyglobby';
|
|
6
|
+
import { linkResolverOptions } from '../link-options.js';
|
|
7
|
+
import { collectLinkInputs } from '../links-io.js';
|
|
8
|
+
export async function linksCommand(args, io) {
|
|
9
|
+
const { values, positionals } = parseArgs({
|
|
10
|
+
args,
|
|
11
|
+
allowPositionals: true,
|
|
12
|
+
options: {
|
|
13
|
+
out: { type: 'string' },
|
|
14
|
+
fix: { type: 'boolean', default: false },
|
|
15
|
+
'link-resolve': { type: 'string' },
|
|
16
|
+
'locale-field': { type: 'string' },
|
|
17
|
+
'slug-field': { type: 'string' },
|
|
18
|
+
'key-field': { type: 'string' },
|
|
19
|
+
'default-locale': { type: 'string' },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (positionals.length === 0) {
|
|
23
|
+
io.stderr('links: provide at least one file or glob.');
|
|
24
|
+
return 2;
|
|
25
|
+
}
|
|
26
|
+
const files = (await glob(positionals, { absolute: true })).sort();
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
io.stderr(`links: no files matched ${positionals.join(' ')}`);
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
const inputs = await collectLinkInputs(files);
|
|
32
|
+
const linkOptions = linkResolverOptions(values);
|
|
33
|
+
let errors = 0;
|
|
34
|
+
let warnings = 0;
|
|
35
|
+
for (const { file, diagnostic } of validateLinks(inputs, linkOptions)) {
|
|
36
|
+
io.stderr(formatDiagnostic(diagnostic, file));
|
|
37
|
+
if (diagnostic.severity === 'error')
|
|
38
|
+
errors++;
|
|
39
|
+
else if (diagnostic.severity === 'warning')
|
|
40
|
+
warnings++;
|
|
41
|
+
}
|
|
42
|
+
const index = buildLinkIndex(inputs, linkOptions);
|
|
43
|
+
if (values.fix && errors > 0) {
|
|
44
|
+
io.stderr('links: --fix skipped because link errors must be resolved first.');
|
|
45
|
+
}
|
|
46
|
+
else if (values.fix && index.aliases.size > 0) {
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const source = await readFile(file, 'utf8');
|
|
49
|
+
const fm = extractFrontmatter(source);
|
|
50
|
+
if (!fm)
|
|
51
|
+
continue;
|
|
52
|
+
const lines = source.split('\n');
|
|
53
|
+
let changed = false;
|
|
54
|
+
// Only rewrite alias tokens inside the `linksTo:` block — never inside an
|
|
55
|
+
// `aliases:` list (which records the rename and must stay intact), and
|
|
56
|
+
// never in document body. Track whether we are within linksTo's scope:
|
|
57
|
+
// a top-level key resets it, dash-list / inline continuation keeps it.
|
|
58
|
+
let inLinksTo = false;
|
|
59
|
+
for (let i = 0; i < fm.lines.end && i < lines.length; i++) {
|
|
60
|
+
const line = lines[i];
|
|
61
|
+
const topKey = line.match(/^([A-Za-z0-9_.-]+):(.*)$/);
|
|
62
|
+
if (topKey)
|
|
63
|
+
inLinksTo = topKey[1] === 'linksTo';
|
|
64
|
+
if (!inLinksTo)
|
|
65
|
+
continue;
|
|
66
|
+
let next = line;
|
|
67
|
+
for (const [alias, current] of aliasReplacementsForPage(index, fm.data)) {
|
|
68
|
+
const re = new RegExp(`(^|[\\s\\[,'"-])${escapeRe(alias)}($|[\\s\\],'"])`, 'g');
|
|
69
|
+
next = next.replace(re, (_m, p1, p2) => `${p1}${current}${p2}`);
|
|
70
|
+
}
|
|
71
|
+
if (next !== line) {
|
|
72
|
+
lines[i] = next;
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (changed) {
|
|
77
|
+
await io.writeFile(file, lines.join('\n'));
|
|
78
|
+
io.stdout(`fixed alias references in ${file}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const outPath = values.out ?? join(process.cwd(), '.contentbit', 'link-index.json');
|
|
83
|
+
// The default target lives in a .contentbit/ dir that may not exist yet, and
|
|
84
|
+
// the shared Io.writeFile is a thin fs wrapper that won't create it.
|
|
85
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
86
|
+
await io.writeFile(outPath, JSON.stringify(serializeLinkIndex(index), null, 2) + '\n');
|
|
87
|
+
let edges = 0;
|
|
88
|
+
for (const p of index.pages.values())
|
|
89
|
+
edges += p.linksTo.length;
|
|
90
|
+
const orphans = [...index.pages.values()].filter((p) => p.linkedFrom.length === 0).length;
|
|
91
|
+
io.stdout(`${index.pages.size} page(s), ${edges} link(s), ${orphans} orphan(s): ${errors} errors, ${warnings} warnings`);
|
|
92
|
+
io.stdout(`index written to ${outPath}`);
|
|
93
|
+
return errors > 0 ? 1 : 0;
|
|
94
|
+
}
|
|
95
|
+
function escapeRe(s) {
|
|
96
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6B3E"}
|
package/dist/commands/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { genericMarkdownRenderers } from '@contentbit/blocks';
|
|
2
|
-
import { formatDiagnostic, parseDocument, renderToMarkdown, validateDocument, } from '@contentbit/core';
|
|
2
|
+
import { formatDiagnostic, parseDocument, renderToMarkdown, stripFrontmatter, validateDocument, } from '@contentbit/core';
|
|
3
3
|
import { renderToHtml } from '@contentbit/html';
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
@@ -21,7 +21,7 @@ export async function renderCommand(args, io) {
|
|
|
21
21
|
}
|
|
22
22
|
const registry = await loadRegistry(values.registry);
|
|
23
23
|
const source = await readFile(file, 'utf8');
|
|
24
|
-
const result = validateDocument(parseDocument(source), registry);
|
|
24
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry);
|
|
25
25
|
if (!result.ok) {
|
|
26
26
|
for (const d of result.diagnostics)
|
|
27
27
|
io.stderr(formatDiagnostic(d, file));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAKnC,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwD7E"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { formatDiagnostic, parseDocument, stripFrontmatter, validateDocument, } from '@contentbit/core';
|
|
1
|
+
import { extractFrontmatter, formatDiagnostic, parseDocument, stripFrontmatter, validateDocument, validateLinks, } from '@contentbit/core';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { glob } from 'tinyglobby';
|
|
5
|
+
import { linkResolverOptions } from '../link-options.js';
|
|
5
6
|
import { loadRegistry } from '../load-registry.js';
|
|
6
7
|
export async function validateCommand(args, io) {
|
|
7
8
|
const { values, positionals } = parseArgs({
|
|
@@ -10,6 +11,11 @@ export async function validateCommand(args, io) {
|
|
|
10
11
|
options: {
|
|
11
12
|
registry: { type: 'string' },
|
|
12
13
|
'strict-warnings': { type: 'boolean', default: false },
|
|
14
|
+
'link-resolve': { type: 'string' },
|
|
15
|
+
'locale-field': { type: 'string' },
|
|
16
|
+
'slug-field': { type: 'string' },
|
|
17
|
+
'key-field': { type: 'string' },
|
|
18
|
+
'default-locale': { type: 'string' },
|
|
13
19
|
},
|
|
14
20
|
});
|
|
15
21
|
if (positionals.length === 0) {
|
|
@@ -22,13 +28,16 @@ export async function validateCommand(args, io) {
|
|
|
22
28
|
return 2;
|
|
23
29
|
}
|
|
24
30
|
const registry = await loadRegistry(values.registry);
|
|
31
|
+
const linkOptions = linkResolverOptions(values);
|
|
25
32
|
let errors = 0;
|
|
26
33
|
let warnings = 0;
|
|
34
|
+
const linkInputs = [];
|
|
27
35
|
for (const file of files.sort()) {
|
|
28
36
|
const source = await readFile(file, 'utf8');
|
|
29
37
|
// Frontmatter is metadata, not content: blanked (positions preserved) so
|
|
30
38
|
// block syntax inside YAML never produces diagnostics — matching what
|
|
31
39
|
// frontmatter-aware consumers like Astro validate from entry bodies.
|
|
40
|
+
linkInputs.push({ path: file, data: extractFrontmatter(source)?.data ?? {} });
|
|
32
41
|
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry);
|
|
33
42
|
for (const d of result.diagnostics) {
|
|
34
43
|
io.stderr(formatDiagnostic(d, file));
|
|
@@ -38,6 +47,16 @@ export async function validateCommand(args, io) {
|
|
|
38
47
|
warnings++;
|
|
39
48
|
}
|
|
40
49
|
}
|
|
50
|
+
// Cross-file internal-link checks, only when the project uses linking.
|
|
51
|
+
if (linkInputs.some((i) => 'slug' in i.data)) {
|
|
52
|
+
for (const { file, diagnostic } of validateLinks(linkInputs, linkOptions)) {
|
|
53
|
+
io.stderr(formatDiagnostic(diagnostic, file));
|
|
54
|
+
if (diagnostic.severity === 'error')
|
|
55
|
+
errors++;
|
|
56
|
+
else if (diagnostic.severity === 'warning')
|
|
57
|
+
warnings++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
41
60
|
io.stdout(`${files.length} file(s): ${errors} errors, ${warnings} warnings`);
|
|
42
61
|
if (errors > 0)
|
|
43
62
|
return 1;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LinkResolverOptions } from '@contentbit/core';
|
|
2
|
+
export interface LinkOptionValues {
|
|
3
|
+
'link-resolve'?: string | boolean;
|
|
4
|
+
'locale-field'?: string | boolean;
|
|
5
|
+
'slug-field'?: string | boolean;
|
|
6
|
+
'key-field'?: string | boolean;
|
|
7
|
+
'default-locale'?: string | boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function linkResolverOptions(values: LinkOptionValues): LinkResolverOptions;
|
|
10
|
+
//# sourceMappingURL=link-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-options.d.ts","sourceRoot":"","sources":["../src/link-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACpC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,GAAG,mBAAmB,CAgBjF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function linkResolverOptions(values) {
|
|
2
|
+
const out = {};
|
|
3
|
+
const resolve = stringValue(values['link-resolve']);
|
|
4
|
+
if (resolve) {
|
|
5
|
+
if (!isResolveMode(resolve))
|
|
6
|
+
throw new Error(`invalid --link-resolve ${resolve}`);
|
|
7
|
+
out.resolve = resolve;
|
|
8
|
+
}
|
|
9
|
+
const localeField = stringValue(values['locale-field']);
|
|
10
|
+
const slugField = stringValue(values['slug-field']);
|
|
11
|
+
const keyField = stringValue(values['key-field']);
|
|
12
|
+
const defaultLocale = stringValue(values['default-locale']);
|
|
13
|
+
if (localeField)
|
|
14
|
+
out.localeField = localeField;
|
|
15
|
+
if (slugField)
|
|
16
|
+
out.slugField = slugField;
|
|
17
|
+
if (keyField)
|
|
18
|
+
out.keyField = keyField;
|
|
19
|
+
if (defaultLocale)
|
|
20
|
+
out.defaultLocale = defaultLocale;
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function stringValue(value) {
|
|
24
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function isResolveMode(value) {
|
|
27
|
+
return (value === 'global-slug' ||
|
|
28
|
+
value === 'same-locale-slug' ||
|
|
29
|
+
value === 'same-locale-key' ||
|
|
30
|
+
value === 'prefer-same-locale-key-fallback-slug');
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links-io.d.ts","sourceRoot":"","sources":["../src/links-io.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAMrE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAQ7E"}
|
package/dist/links-io.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { extractFrontmatter } from '@contentbit/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
// Read each file's frontmatter (head only — bodies are never parsed) into the
|
|
4
|
+
// LinkInput shape the core link functions consume. Files with no frontmatter
|
|
5
|
+
// contribute an empty data object (a non-participating page).
|
|
6
|
+
export async function collectLinkInputs(files) {
|
|
7
|
+
const inputs = [];
|
|
8
|
+
for (const path of files) {
|
|
9
|
+
const source = await readFile(path, 'utf8');
|
|
10
|
+
const fm = extractFrontmatter(source);
|
|
11
|
+
inputs.push({ path, data: fm?.data ?? {} });
|
|
12
|
+
}
|
|
13
|
+
return inputs;
|
|
14
|
+
}
|
package/dist/run.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export interface Io {
|
|
|
3
3
|
stderr(line: string): void;
|
|
4
4
|
writeFile(path: string, content: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
export declare const USAGE = "Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]\n\n init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]\n agents [--claude] [--no-agents-md]\n\n validate <globs...> [--registry <module.mjs>] [--strict-warnings]\n stats <globs...> [--registry <module.mjs>] [--no-validate]\n render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]\n instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]\n docs [--registry <module.mjs>] [--out <file>]";
|
|
6
|
+
export declare const USAGE = "Usage: contentbit <init|validate|stats|render|instructions|docs|agents|links> [options]\n\n init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]\n agents [--claude] [--no-agents-md]\n\n validate <globs...> [--registry <module.mjs>] [--strict-warnings] [--link-resolve <mode>]\n stats <globs...> [--registry <module.mjs>] [--no-validate]\n render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]\n instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]\n docs [--registry <module.mjs>] [--out <file>]\n links <globs...> [--fix] [--out <file>] [--link-resolve <mode>]";
|
|
7
7
|
export declare function run(argv: string[], io: Io): Promise<number>;
|
|
8
8
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,EAAE;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxD;AAED,eAAO,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,EAAE;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxD;AAED,eAAO,MAAM,KAAK,sqBAUgD,CAAA;AAelE,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcjE"}
|