@ijonis/geo-lint 0.1.4 → 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.
- package/CHANGELOG.md +47 -0
- package/README.md +58 -7
- package/dist/cli.cjs +195 -8
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +195 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +195 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +195 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `seo-schema-sameas-incomplete` — flags Organization schema with fewer than 2 sameAs entries for entity verification
|
|
14
|
+
- `geo-author-not-person` — flags blog posts with organization names as author instead of a person
|
|
15
|
+
- `seo-service-page-no-schema` — flags service pages without Service structured data
|
|
16
|
+
- `technical-no-feed` — flags sites without RSS/Atom/JSON feeds declared in config
|
|
17
|
+
- `technical-no-llms-txt` — flags sites without /llms.txt endpoint declared in config
|
|
18
|
+
- New config fields: `geo.organizationSameAs`, `geo.servicePagePatterns`
|
|
19
|
+
- New `technical` config section with `feedUrls` and `llmsTxtUrl`
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Rule count increased from 92 to 97
|
|
23
|
+
- `schema-rules.ts` now uses factory pattern (`createSchemaRules`) for config-aware rules
|
|
24
|
+
|
|
25
|
+
## [0.1.6] - 2026-03-03
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Claude Code skill plugin for ecosystem distribution — geo-lint is now installable as a Claude Code skill via `/install-skill`
|
|
29
|
+
- `/content-creator` skill for self-configuring content pipelines — auto-detects project framework, generates adapter scripts, and runs iterative lint-fix loops
|
|
30
|
+
|
|
31
|
+
## [0.1.5] - 2026-02-20
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- CLI now always exits with code `0` regardless of violation severity — the linter is advisory-only and never blocks CI/CD pipelines. Error/warning labels are preserved for prioritization.
|
|
35
|
+
|
|
36
|
+
## [0.1.4] - 2026-02-20
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Full English/German language parity across all analyzers
|
|
40
|
+
- Locale-aware Flesch Reading Ease: EN formula (`206.835 - 1.015*ASL - 84.6*ASW`) and DE formula (`180 - ASL - 58.5*ASW`) with per-locale interpretation bands
|
|
41
|
+
- `estimateSyllables()` applies silent-e discount for English and compound-word adjustment for German
|
|
42
|
+
- `defaultLocale` added to `RuleContext`, populated from `config.i18n`
|
|
43
|
+
- Unified locale fallback chain: `item.locale` → `context.defaultLocale` → hardcoded default
|
|
44
|
+
- Removed hardcoded German assumption in slug-resolver permalink generation
|
|
45
|
+
- 15 new readability tests covering both language formulas
|
|
46
|
+
|
|
47
|
+
## [0.1.3] - 2026-02-20
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- README "Why this exists" section rewritten with the real motivation: running multiple content-heavy sites with no deterministic SEO/GEO validation tool
|
|
51
|
+
- Stronger value framing in `docs/geo-rules.md` and `docs/rules.md`
|
|
52
|
+
|
|
10
53
|
## [0.1.2] - 2026-02-20
|
|
11
54
|
|
|
12
55
|
### Fixed
|
|
@@ -43,6 +86,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
43
86
|
- MDX/Markdown content adapter with `gray-matter`
|
|
44
87
|
- CLI with `--format=json`, `--rules`, `--root`, `--config` flags
|
|
45
88
|
|
|
89
|
+
[0.1.6]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.6
|
|
90
|
+
[0.1.5]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.5
|
|
91
|
+
[0.1.4]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.4
|
|
92
|
+
[0.1.3]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.3
|
|
46
93
|
[0.1.2]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.2
|
|
47
94
|
[0.1.1]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.1
|
|
48
95
|
[0.1.0]: https://github.com/IJONIS/geo-lint/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -5,11 +5,55 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ijonis/geo-lint)
|
|
6
6
|
[](https://github.com/IJONIS/geo-lint/actions)
|
|
7
7
|
[](https://github.com/IJONIS/geo-lint/blob/main/LICENSE)
|
|
8
|
+
[](https://github.com/IJONIS/geo-lint)
|
|
8
9
|
|
|
9
10
|

|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
14
|
+
## Use with Claude Code
|
|
15
|
+
|
|
16
|
+
Install the geo-lint skill in one command:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -fsSL https://raw.githubusercontent.com/IJONIS/geo-lint/main/install.sh | bash
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then in any Claude Code session:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/geo-lint audit # Full sweep — find and fix all violations
|
|
26
|
+
/geo-lint fix <slug> # Fix a single content file
|
|
27
|
+
/geo-lint rules # Show all 97 rules with fix strategies
|
|
28
|
+
/geo-lint init # Set up geo-lint.config.ts for a new project
|
|
29
|
+
/geo-lint report # Generate a GEO/SEO health summary
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The skill runs an autonomous lint-fix loop: scan your content, read structured violations, fix them using each violation's `suggestion` field, and re-lint until clean. Parallel subagents handle multiple files simultaneously.
|
|
33
|
+
|
|
34
|
+
### Content Creation Pipeline
|
|
35
|
+
|
|
36
|
+
The install also adds `/content-creator` -- a self-configuring content pipeline that creates SEO & GEO-optimized content matched to your project:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
/content-creator setup # Auto-discover your project + configure brand voice
|
|
40
|
+
/content-creator create # Create content with keyword research + validation
|
|
41
|
+
/content-creator voice # Analyze and adjust brand voice
|
|
42
|
+
/content-creator calendar # Plan monthly content calendar
|
|
43
|
+
/content-creator refresh # Update config when your project evolves
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
On first use, the skill scans your project to learn its framework, content schema, categories, and authors. It then asks a few questions about your brand voice and audience. From that point on, every piece of content is created with full project awareness and validated against geo-lint's 97 rules until clean -- zero manual intervention for mechanical quality.
|
|
47
|
+
|
|
48
|
+
Or paste this into **any** AI agent (Claude Code, Cursor, Windsurf, Copilot):
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Run npx geo-lint --format=json, then fix every violation using each
|
|
52
|
+
violation's suggestion field. Re-run until the output is [].
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
13
57
|
## Why this exists
|
|
14
58
|
|
|
15
59
|
I run multiple content-heavy sites and there was no deterministic way to validate whether my content was actually optimized -- not "probably fine," but actually checked against concrete rules. SEO linters exist, but they're either paid SaaS, not automatable, or completely ignore the structural patterns that AI search engines use when deciding what to cite.
|
|
@@ -18,7 +62,7 @@ So I built one. **GEO (Generative Engine Optimization)** is the practice of stru
|
|
|
18
62
|
|
|
19
63
|
The goal was simple: install one tool, point your AI agent at it, and walk away. The agent runs the linter, reads the JSON violations, fixes the content, re-lints until clean -- across an entire site, no manual input. One command, both SEO and GEO validated.
|
|
20
64
|
|
|
21
|
-
**
|
|
65
|
+
**97 rules: 36 GEO, 34 SEO, 14 content quality, 10 technical, 3 i18n.** Readability analysis inspired by Yoast SEO. We researched the current state of GEO and AEO to make sure the rules reflect what actually gets content cited -- not outdated advice.
|
|
22
66
|
|
|
23
67
|
---
|
|
24
68
|
|
|
@@ -117,15 +161,15 @@ components at build time.
|
|
|
117
161
|
|
|
118
162
|
---
|
|
119
163
|
|
|
120
|
-
## All
|
|
164
|
+
## All 97 Rules
|
|
121
165
|
|
|
122
166
|
| Category | Rules | Severity Mix | Focus |
|
|
123
167
|
|----------|-------|-------------|-------|
|
|
124
|
-
| SEO |
|
|
168
|
+
| SEO | 34 | 6 errors, 28 warnings | Titles, descriptions, headings, slugs, OG images, canonical URLs, keywords, links, schema, sameAs, service pages |
|
|
125
169
|
| Content | 14 | 2 errors, 12 warnings | Word count, readability, dates, categories, jargon density, repetition, vocabulary diversity, transition words, sentence variety |
|
|
126
|
-
| Technical |
|
|
170
|
+
| Technical | 10 | 3 errors, 7 warnings | Broken links, image files, trailing slashes, external URLs, performance, feeds, llms.txt |
|
|
127
171
|
| i18n | 3 | 0 errors, 3 warnings | Translation pairs, locale metadata |
|
|
128
|
-
| GEO |
|
|
172
|
+
| GEO | 36 | 0 errors, 36 warnings | AI citation readiness: E-E-A-T signals, content structure, freshness, RAG optimization, author entity type |
|
|
129
173
|
|
|
130
174
|
See the [complete rule reference](docs/rules.md) with descriptions and severity for every rule.
|
|
131
175
|
|
|
@@ -147,9 +191,13 @@ See the [complete rule reference](docs/rules.md) with descriptions and severity
|
|
|
147
191
|
|
|
148
192
|
This linter is **deterministic** -- same content in, same violations out, every time. Your AI agent provides the creativity to fix the content; geo-lint provides the guardrails to verify it's correct. The loop runs until violations hit zero.
|
|
149
193
|
|
|
150
|
-
###
|
|
194
|
+
### Claude Code Plugin
|
|
151
195
|
|
|
152
|
-
|
|
196
|
+
Install the skill and use `/geo-lint audit` to validate and fix your entire content directory automatically. The skill runs the full lint-fix loop with parallel subagents -- one per file. See [Use with Claude Code](#use-with-claude-code) above.
|
|
197
|
+
|
|
198
|
+
### Any AI Agent
|
|
199
|
+
|
|
200
|
+
Paste this into **Cursor**, **Windsurf**, **Copilot**, or any AI coding agent:
|
|
153
201
|
|
|
154
202
|
```
|
|
155
203
|
Run npx geo-lint --format=json, then fix every violation in the reported
|
|
@@ -265,6 +313,9 @@ Options:
|
|
|
265
313
|
|
|
266
314
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and how to add new rules. Changes are tracked in the [CHANGELOG](CHANGELOG.md).
|
|
267
315
|
|
|
316
|
+
**Questions or ideas?** Open a [GitHub Discussion](https://github.com/IJONIS/geo-lint/discussions).
|
|
317
|
+
**Bugs or feature requests?** Open a [GitHub Issue](https://github.com/IJONIS/geo-lint/issues).
|
|
318
|
+
|
|
268
319
|
## License
|
|
269
320
|
|
|
270
321
|
[MIT](LICENSE)
|
package/dist/cli.cjs
CHANGED
|
@@ -179,7 +179,13 @@ var DEFAULT_CONFIG = {
|
|
|
179
179
|
"Tabs",
|
|
180
180
|
"Tab"
|
|
181
181
|
],
|
|
182
|
-
enabledContentTypes: ["blog"]
|
|
182
|
+
enabledContentTypes: ["blog"],
|
|
183
|
+
organizationSameAs: [],
|
|
184
|
+
servicePagePatterns: []
|
|
185
|
+
},
|
|
186
|
+
technical: {
|
|
187
|
+
feedUrls: [],
|
|
188
|
+
llmsTxtUrl: ""
|
|
183
189
|
},
|
|
184
190
|
i18n: {
|
|
185
191
|
locales: ["de", "en"],
|
|
@@ -251,12 +257,18 @@ function mergeWithDefaults(user) {
|
|
|
251
257
|
acronymAllowlist: user.geo?.acronymAllowlist ?? DEFAULT_CONFIG.geo.acronymAllowlist,
|
|
252
258
|
vagueHeadings: user.geo?.vagueHeadings ?? DEFAULT_CONFIG.geo.vagueHeadings,
|
|
253
259
|
genericAuthorNames: user.geo?.genericAuthorNames ?? DEFAULT_CONFIG.geo.genericAuthorNames,
|
|
254
|
-
allowedHtmlTags: user.geo?.allowedHtmlTags ?? DEFAULT_CONFIG.geo.allowedHtmlTags
|
|
260
|
+
allowedHtmlTags: user.geo?.allowedHtmlTags ?? DEFAULT_CONFIG.geo.allowedHtmlTags,
|
|
261
|
+
organizationSameAs: user.geo?.organizationSameAs ?? DEFAULT_CONFIG.geo.organizationSameAs,
|
|
262
|
+
servicePagePatterns: user.geo?.servicePagePatterns ?? DEFAULT_CONFIG.geo.servicePagePatterns
|
|
255
263
|
},
|
|
256
264
|
i18n: {
|
|
257
265
|
locales: user.i18n?.locales ?? DEFAULT_CONFIG.i18n.locales,
|
|
258
266
|
defaultLocale: user.i18n?.defaultLocale ?? DEFAULT_CONFIG.i18n.defaultLocale
|
|
259
267
|
},
|
|
268
|
+
technical: {
|
|
269
|
+
feedUrls: user.technical?.feedUrls ?? DEFAULT_CONFIG.technical.feedUrls,
|
|
270
|
+
llmsTxtUrl: user.technical?.llmsTxtUrl ?? DEFAULT_CONFIG.technical.llmsTxtUrl
|
|
271
|
+
},
|
|
260
272
|
rules: { ...DEFAULT_CONFIG.rules, ...user.rules ?? {} },
|
|
261
273
|
thresholds: {
|
|
262
274
|
title: { ...DEFAULT_CONFIG.thresholds.title, ...user.thresholds?.title ?? {} },
|
|
@@ -2607,12 +2619,72 @@ var datasetSchemaReadiness = {
|
|
|
2607
2619
|
return results;
|
|
2608
2620
|
}
|
|
2609
2621
|
};
|
|
2610
|
-
var
|
|
2622
|
+
var MIN_SAMEAS_ENTRIES = 2;
|
|
2623
|
+
function createSchemaSameAsRule(organizationSameAs) {
|
|
2624
|
+
let hasFired = false;
|
|
2625
|
+
return {
|
|
2626
|
+
name: "seo-schema-sameas-incomplete",
|
|
2627
|
+
severity: "warning",
|
|
2628
|
+
category: "seo",
|
|
2629
|
+
fixStrategy: "Add social profiles (LinkedIn, GitHub, Twitter), Wikidata QID, and Crunchbase URL to Organization schema sameAs array",
|
|
2630
|
+
run: (_item) => {
|
|
2631
|
+
if (hasFired) return [];
|
|
2632
|
+
hasFired = true;
|
|
2633
|
+
if (!organizationSameAs || organizationSameAs.length === 0) return [];
|
|
2634
|
+
if (organizationSameAs.length < MIN_SAMEAS_ENTRIES) {
|
|
2635
|
+
return [
|
|
2636
|
+
{
|
|
2637
|
+
file: "_site",
|
|
2638
|
+
field: "schema",
|
|
2639
|
+
rule: "seo-schema-sameas-incomplete",
|
|
2640
|
+
severity: "warning",
|
|
2641
|
+
message: `Organization sameAs has ${organizationSameAs.length} entry \u2014 include at least ${MIN_SAMEAS_ENTRIES} for entity verification`,
|
|
2642
|
+
suggestion: "AI models use sameAs to verify entity identity. Include at least LinkedIn + one other profile (GitHub, Wikidata QID, Crunchbase)."
|
|
2643
|
+
}
|
|
2644
|
+
];
|
|
2645
|
+
}
|
|
2646
|
+
return [];
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
function createServicePageSchemaRule(servicePagePatterns) {
|
|
2651
|
+
return {
|
|
2652
|
+
name: "seo-service-page-no-schema",
|
|
2653
|
+
severity: "warning",
|
|
2654
|
+
category: "seo",
|
|
2655
|
+
fixStrategy: "Add Service structured data (JSON-LD) to service pages with name, description, provider, and areaServed.",
|
|
2656
|
+
run: (item) => {
|
|
2657
|
+
if (!servicePagePatterns || servicePagePatterns.length === 0) return [];
|
|
2658
|
+
const matchesPattern = servicePagePatterns.some(
|
|
2659
|
+
(pattern) => item.permalink.includes(pattern)
|
|
2660
|
+
);
|
|
2661
|
+
if (!matchesPattern) return [];
|
|
2662
|
+
return [
|
|
2663
|
+
{
|
|
2664
|
+
file: getDisplayPath(item),
|
|
2665
|
+
field: "schema",
|
|
2666
|
+
rule: "seo-service-page-no-schema",
|
|
2667
|
+
severity: "warning",
|
|
2668
|
+
message: `Service page "${item.permalink}" should have Service structured data`,
|
|
2669
|
+
suggestion: 'Service pages need schema markup to appear in AI answers for "[service] provider in [city]" queries. Add Service JSON-LD with name, description, provider, and areaServed.'
|
|
2670
|
+
}
|
|
2671
|
+
];
|
|
2672
|
+
}
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
var schemaStaticRules = [
|
|
2611
2676
|
blogMissingSchemaFields,
|
|
2612
2677
|
faqpageSchemaReadiness,
|
|
2613
2678
|
breadcrumblistSchemaReadiness,
|
|
2614
2679
|
datasetSchemaReadiness
|
|
2615
2680
|
];
|
|
2681
|
+
function createSchemaRules(geo) {
|
|
2682
|
+
return [
|
|
2683
|
+
...schemaStaticRules,
|
|
2684
|
+
createSchemaSameAsRule(geo.organizationSameAs),
|
|
2685
|
+
createServicePageSchemaRule(geo.servicePagePatterns)
|
|
2686
|
+
];
|
|
2687
|
+
}
|
|
2616
2688
|
|
|
2617
2689
|
// src/rules/keyword-coherence-rules.ts
|
|
2618
2690
|
var MIN_SIGNIFICANT_WORDS = 2;
|
|
@@ -14644,6 +14716,58 @@ var geoMissingTldr = {
|
|
|
14644
14716
|
return [];
|
|
14645
14717
|
}
|
|
14646
14718
|
};
|
|
14719
|
+
var ORG_AUTHOR_PATTERNS = [
|
|
14720
|
+
/\bteam\b/i,
|
|
14721
|
+
/\bredaktion\b/i,
|
|
14722
|
+
/\beditorial\b/i,
|
|
14723
|
+
/\beditors?\b/i,
|
|
14724
|
+
/\bherausgeber\b/i,
|
|
14725
|
+
/\bverlag\b/i,
|
|
14726
|
+
/\bredaktionsteam\b/i
|
|
14727
|
+
];
|
|
14728
|
+
function createGeoAuthorNotPersonRule(brandName) {
|
|
14729
|
+
return {
|
|
14730
|
+
name: "geo-author-not-person",
|
|
14731
|
+
severity: "warning",
|
|
14732
|
+
category: "geo",
|
|
14733
|
+
fixStrategy: "Replace organization name with individual author name. Use Person type in BlogPosting schema for stronger E-E-A-T signals.",
|
|
14734
|
+
run: (item, context) => {
|
|
14735
|
+
const geoTypes = context.geoEnabledContentTypes ?? ["blog"];
|
|
14736
|
+
if (!geoTypes.includes(item.contentType)) return [];
|
|
14737
|
+
if (!item.author || item.author.trim() === "") return [];
|
|
14738
|
+
if (!brandName || brandName.trim() === "") return [];
|
|
14739
|
+
const normalizedAuthor = item.author.trim().toLowerCase();
|
|
14740
|
+
if (normalizedAuthor === brandName.trim().toLowerCase()) {
|
|
14741
|
+
return [
|
|
14742
|
+
{
|
|
14743
|
+
file: getDisplayPath(item),
|
|
14744
|
+
field: "author",
|
|
14745
|
+
rule: "geo-author-not-person",
|
|
14746
|
+
severity: "warning",
|
|
14747
|
+
message: `Author "${item.author}" is the organization name \u2014 use a person's name instead`,
|
|
14748
|
+
suggestion: "AI models cite named experts over faceless organizations. Use the actual author's name for stronger E-E-A-T signals."
|
|
14749
|
+
}
|
|
14750
|
+
];
|
|
14751
|
+
}
|
|
14752
|
+
const matchesOrgPattern = ORG_AUTHOR_PATTERNS.some(
|
|
14753
|
+
(pattern) => pattern.test(item.author)
|
|
14754
|
+
);
|
|
14755
|
+
if (matchesOrgPattern) {
|
|
14756
|
+
return [
|
|
14757
|
+
{
|
|
14758
|
+
file: getDisplayPath(item),
|
|
14759
|
+
field: "author",
|
|
14760
|
+
rule: "geo-author-not-person",
|
|
14761
|
+
severity: "warning",
|
|
14762
|
+
message: `Author "${item.author}" appears to be an organization or team name`,
|
|
14763
|
+
suggestion: "BlogPosting with author.@type: Person gets cited more than Organization. Use an individual person's name."
|
|
14764
|
+
}
|
|
14765
|
+
];
|
|
14766
|
+
}
|
|
14767
|
+
return [];
|
|
14768
|
+
}
|
|
14769
|
+
};
|
|
14770
|
+
}
|
|
14647
14771
|
var geoEeatStaticRules = [
|
|
14648
14772
|
geoMissingSourceCitations,
|
|
14649
14773
|
geoMissingExpertQuotes,
|
|
@@ -14656,7 +14780,8 @@ function createGeoEeatRules(geo) {
|
|
|
14656
14780
|
return [
|
|
14657
14781
|
...geoEeatStaticRules,
|
|
14658
14782
|
createGeoMissingAuthorRule(geo.genericAuthorNames ?? []),
|
|
14659
|
-
createGeoHeadingTooVagueRule(geo.vagueHeadings ?? [])
|
|
14783
|
+
createGeoHeadingTooVagueRule(geo.vagueHeadings ?? []),
|
|
14784
|
+
createGeoAuthorNotPersonRule(geo.brandName)
|
|
14660
14785
|
];
|
|
14661
14786
|
}
|
|
14662
14787
|
|
|
@@ -15645,6 +15770,68 @@ var contentQualityRules = [
|
|
|
15645
15770
|
sentenceVariety
|
|
15646
15771
|
];
|
|
15647
15772
|
|
|
15773
|
+
// src/rules/technical-site-rules.ts
|
|
15774
|
+
function createNoFeedRule(feedUrls) {
|
|
15775
|
+
let hasFired = false;
|
|
15776
|
+
return {
|
|
15777
|
+
name: "technical-no-feed",
|
|
15778
|
+
severity: "warning",
|
|
15779
|
+
category: "technical",
|
|
15780
|
+
fixStrategy: "Add an RSS or JSON feed endpoint exposing blog posts with full content.",
|
|
15781
|
+
run: (_item, _context) => {
|
|
15782
|
+
if (hasFired) return [];
|
|
15783
|
+
hasFired = true;
|
|
15784
|
+
if (feedUrls === void 0) return [];
|
|
15785
|
+
if (feedUrls.length === 0) {
|
|
15786
|
+
return [
|
|
15787
|
+
{
|
|
15788
|
+
file: "_site",
|
|
15789
|
+
field: "feed",
|
|
15790
|
+
rule: "technical-no-feed",
|
|
15791
|
+
severity: "warning",
|
|
15792
|
+
message: "No RSS/Atom/JSON feed detected \u2014 AI systems lose a structured ingestion path",
|
|
15793
|
+
suggestion: "Feeds provide a structured ingestion path for AI systems beyond crawler discovery. Add an RSS or JSON feed endpoint."
|
|
15794
|
+
}
|
|
15795
|
+
];
|
|
15796
|
+
}
|
|
15797
|
+
return [];
|
|
15798
|
+
}
|
|
15799
|
+
};
|
|
15800
|
+
}
|
|
15801
|
+
function createNoLlmsTxtRule(llmsTxtUrl) {
|
|
15802
|
+
let hasFired = false;
|
|
15803
|
+
return {
|
|
15804
|
+
name: "technical-no-llms-txt",
|
|
15805
|
+
severity: "warning",
|
|
15806
|
+
category: "technical",
|
|
15807
|
+
fixStrategy: "Create a /llms.txt endpoint that maps your most important content for LLM consumption in Markdown format.",
|
|
15808
|
+
run: (_item, _context) => {
|
|
15809
|
+
if (hasFired) return [];
|
|
15810
|
+
hasFired = true;
|
|
15811
|
+
if (llmsTxtUrl === void 0) return [];
|
|
15812
|
+
if (llmsTxtUrl.trim() === "") {
|
|
15813
|
+
return [
|
|
15814
|
+
{
|
|
15815
|
+
file: "_site",
|
|
15816
|
+
field: "llms-txt",
|
|
15817
|
+
rule: "technical-no-llms-txt",
|
|
15818
|
+
severity: "warning",
|
|
15819
|
+
message: "No /llms.txt endpoint detected \u2014 missing the emerging standard for LLM content declaration",
|
|
15820
|
+
suggestion: "llms.txt is the robots.txt equivalent for AI \u2014 trivial to add, future-proofs your site for LLM crawlers."
|
|
15821
|
+
}
|
|
15822
|
+
];
|
|
15823
|
+
}
|
|
15824
|
+
return [];
|
|
15825
|
+
}
|
|
15826
|
+
};
|
|
15827
|
+
}
|
|
15828
|
+
function createTechnicalSiteRules(technical) {
|
|
15829
|
+
return [
|
|
15830
|
+
createNoFeedRule(technical.feedUrls),
|
|
15831
|
+
createNoLlmsTxtRule(technical.llmsTxtUrl)
|
|
15832
|
+
];
|
|
15833
|
+
}
|
|
15834
|
+
|
|
15648
15835
|
// src/rules/index.ts
|
|
15649
15836
|
function buildRules(config, linkExtractor) {
|
|
15650
15837
|
const rules = [
|
|
@@ -15664,7 +15851,7 @@ function buildRules(config, linkExtractor) {
|
|
|
15664
15851
|
...createI18nRules(config.i18n),
|
|
15665
15852
|
...dateRules,
|
|
15666
15853
|
...config.categories.length > 0 ? createCategoryRules(config.categories) : [],
|
|
15667
|
-
...
|
|
15854
|
+
...createSchemaRules(config.geo),
|
|
15668
15855
|
...createGeoRules(config.geo),
|
|
15669
15856
|
...createGeoEeatRules(config.geo),
|
|
15670
15857
|
...geoStructureRules,
|
|
@@ -15672,7 +15859,8 @@ function buildRules(config, linkExtractor) {
|
|
|
15672
15859
|
...createGeoRagRules(config.geo),
|
|
15673
15860
|
...keywordCoherenceRules,
|
|
15674
15861
|
...createCanonicalRules(config.siteUrl),
|
|
15675
|
-
...contentQualityRules
|
|
15862
|
+
...contentQualityRules,
|
|
15863
|
+
...createTechnicalSiteRules(config.technical)
|
|
15676
15864
|
];
|
|
15677
15865
|
return rules.map((rule) => applyRuleOverride(rule, config.rules));
|
|
15678
15866
|
}
|
|
@@ -15856,8 +16044,7 @@ async function lint(options = {}) {
|
|
|
15856
16044
|
} else {
|
|
15857
16045
|
formatResults(results, lintableItems.length, excludedItems.length);
|
|
15858
16046
|
}
|
|
15859
|
-
|
|
15860
|
-
return errorCount > 0 ? 1 : 0;
|
|
16047
|
+
return 0;
|
|
15861
16048
|
}
|
|
15862
16049
|
|
|
15863
16050
|
// src/cli.ts
|