geo-ai-search-optimization 1.0.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/LICENSE +21 -0
- package/README.md +58 -0
- package/bin/geo-ai-search-optimization.js +10 -0
- package/package.json +38 -0
- package/resources/geo-ai-search-optimization/SKILL.md +164 -0
- package/resources/geo-ai-search-optimization/agents/openai.yaml +4 -0
- package/resources/geo-ai-search-optimization/references/geo-playbook.md +130 -0
- package/resources/geo-ai-search-optimization/references/implementation-checklist.md +75 -0
- package/resources/geo-ai-search-optimization/scripts/scan-geo-signals.mjs +254 -0
- package/src/cli.js +101 -0
- package/src/index.js +3 -0
- package/src/install-skill.js +49 -0
- package/src/paths.js +33 -0
- package/src/postinstall.js +12 -0
- package/src/scan.js +244 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# geo-ai-search-optimization
|
|
2
|
+
|
|
3
|
+
Install and run a production-ready Codex skill for GEO-first, SEO-supported website optimization.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g geo-ai-search-optimization
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run it without a permanent install:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx geo-ai-search-optimization
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
- installs the bundled `geo-ai-search-optimization` skill into your Codex skills directory
|
|
20
|
+
- ships a local resource folder with `SKILL.md`, references, and a scanner script
|
|
21
|
+
- provides a CLI for installing, locating, and scanning projects for GEO signals
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
geo-ai-search-optimization
|
|
27
|
+
geo-ai-search-optimization install
|
|
28
|
+
geo-ai-search-optimization where
|
|
29
|
+
geo-ai-search-optimization scan ./my-site
|
|
30
|
+
geo-ai-search-optimization help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Install location
|
|
34
|
+
|
|
35
|
+
By default the skill is installed to:
|
|
36
|
+
|
|
37
|
+
- macOS / Linux: `~/.codex/skills/geo-ai-search-optimization`
|
|
38
|
+
- Windows: `%USERPROFILE%\\.codex\\skills\\geo-ai-search-optimization`
|
|
39
|
+
|
|
40
|
+
Override with:
|
|
41
|
+
|
|
42
|
+
- `CODEX_HOME`
|
|
43
|
+
- `CODEX_SKILLS_DIR`
|
|
44
|
+
- `GEO_SKILL_INSTALL_DIR`
|
|
45
|
+
|
|
46
|
+
## Resource contents
|
|
47
|
+
|
|
48
|
+
The installed skill includes:
|
|
49
|
+
|
|
50
|
+
- `SKILL.md`
|
|
51
|
+
- `agents/openai.yaml`
|
|
52
|
+
- `references/geo-playbook.md`
|
|
53
|
+
- `references/implementation-checklist.md`
|
|
54
|
+
- `scripts/scan-geo-signals.mjs`
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "geo-ai-search-optimization",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install and run a GEO-first, SEO-supported Codex skill for AI search optimization.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"geo-ai-search-optimization": "bin/geo-ai-search-optimization.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"resources",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"postinstall": "node ./src/postinstall.js",
|
|
24
|
+
"check": "node ./src/cli.js --help",
|
|
25
|
+
"scan:self": "node ./src/cli.js scan ./resources/geo-ai-search-optimization"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"geo",
|
|
29
|
+
"seo",
|
|
30
|
+
"ai-search",
|
|
31
|
+
"answer-engine-optimization",
|
|
32
|
+
"codex-skill",
|
|
33
|
+
"chatgpt",
|
|
34
|
+
"perplexity",
|
|
35
|
+
"gemini"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geo-ai-search-optimization
|
|
3
|
+
description: Optimize websites for GEO-first, SEO-supported visibility in AI-powered search and answer engines. Use when Codex needs to audit or improve a website, content system, landing page, blog, docs site, ecommerce catalog, or local business presence for ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews, generative engine optimization, answer-engine optimization, AI search discoverability, structured data, citation readiness, or SEO work that must prioritize AI-driven discovery.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GEO AI Search Optimization
|
|
7
|
+
|
|
8
|
+
Optimize for where search traffic is moving: answer engines, AI overviews, and citation-heavy assistants. Preserve classic SEO foundations, but prioritize answerability, source quality, entity clarity, and crawlable evidence.
|
|
9
|
+
|
|
10
|
+
## Core Positioning
|
|
11
|
+
|
|
12
|
+
Treat GEO as the primary objective and SEO as the support layer.
|
|
13
|
+
|
|
14
|
+
Use this skill to:
|
|
15
|
+
|
|
16
|
+
- audit an existing site or repo for AI-search readiness
|
|
17
|
+
- rewrite pages so they are easier for models to quote, summarize, and cite
|
|
18
|
+
- plan new content systems around questions, entities, proof, and comparisons
|
|
19
|
+
- implement technical supports such as schema, canonicals, robots, sitemaps, authorship, and citation surfaces
|
|
20
|
+
|
|
21
|
+
Do not optimize for keyword stuffing, thin FAQ spam, doorway pages, fake expertise, or generic AI-generated filler.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
### 1. Classify the task
|
|
26
|
+
|
|
27
|
+
Choose one primary mode before making recommendations:
|
|
28
|
+
|
|
29
|
+
- `audit`: inspect an existing site, codebase, or exported HTML
|
|
30
|
+
- `rewrite`: improve one or more pages for answer-engine visibility
|
|
31
|
+
- `build`: create a GEO content plan, template system, or implementation brief
|
|
32
|
+
- `measure`: define observable signals, KPIs, and change tracking
|
|
33
|
+
|
|
34
|
+
If the user gives both content and code, start with `audit`, then branch into `rewrite` or `build`.
|
|
35
|
+
|
|
36
|
+
### 2. Establish the evidence surface
|
|
37
|
+
|
|
38
|
+
Look for the assets that make answers trustworthy and quotable:
|
|
39
|
+
|
|
40
|
+
- named entities: brand, product, person, company, location, category
|
|
41
|
+
- explicit claims: pricing, specs, definitions, comparisons, policies, methodology
|
|
42
|
+
- first-party evidence: research, data, benchmarks, screenshots, customer examples, case studies
|
|
43
|
+
- source transparency: dates, authors, reviewers, citations, outbound references
|
|
44
|
+
|
|
45
|
+
If these are weak, say so clearly. AI-search performance often fails because there is nothing distinctive to cite.
|
|
46
|
+
|
|
47
|
+
### 3. Check technical foundations
|
|
48
|
+
|
|
49
|
+
Confirm the site is easy to crawl, interpret, and attribute:
|
|
50
|
+
|
|
51
|
+
- indexable and canonical URLs
|
|
52
|
+
- sensible internal linking and stable information architecture
|
|
53
|
+
- `robots.txt`, sitemap support, and predictable URL patterns
|
|
54
|
+
- strong titles, descriptions, headings, and on-page summaries
|
|
55
|
+
- JSON-LD or equivalent structured data where it helps disambiguation
|
|
56
|
+
- visible authorship, freshness, and organization identity
|
|
57
|
+
|
|
58
|
+
For repo-based work, run the local scanner first:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node scripts/scan-geo-signals.mjs /path/to/project
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or, if the npm package is available:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx geo-ai-search-optimization scan /path/to/project
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use the scanner output as a starting map, not as the final judgment.
|
|
71
|
+
|
|
72
|
+
### 4. Improve answerability before chasing rankings
|
|
73
|
+
|
|
74
|
+
Restructure content so a model can extract a high-confidence answer quickly:
|
|
75
|
+
|
|
76
|
+
- put the direct answer near the top
|
|
77
|
+
- define the entity before expanding on it
|
|
78
|
+
- use specific headings that mirror real user questions
|
|
79
|
+
- add short summary blocks, comparison tables, pros and cons, and decision criteria
|
|
80
|
+
- include citations or clear sourcing for non-obvious claims
|
|
81
|
+
- separate facts, opinions, and marketing language
|
|
82
|
+
|
|
83
|
+
When useful, add FAQ, glossary, alternatives, comparison, methodology, statistics, or policy sections. Prefer dense, useful blocks over long intros.
|
|
84
|
+
|
|
85
|
+
### 5. Strengthen citation readiness
|
|
86
|
+
|
|
87
|
+
Increase the chance that answer engines can quote or paraphrase accurately:
|
|
88
|
+
|
|
89
|
+
- attribute content to real experts, teams, or organizations
|
|
90
|
+
- expose original data, unique process details, or product-specific knowledge
|
|
91
|
+
- link to primary or authoritative sources when claims are not first-party
|
|
92
|
+
- add `sameAs`, organization, author, article, FAQ, product, local business, or breadcrumb schema when relevant
|
|
93
|
+
- make dates, update cadence, and version context explicit
|
|
94
|
+
|
|
95
|
+
If content sounds interchangeable with competitors, call that out and recommend a stronger point of view or original evidence.
|
|
96
|
+
|
|
97
|
+
### 6. Produce prioritized output
|
|
98
|
+
|
|
99
|
+
Always return recommendations in this order:
|
|
100
|
+
|
|
101
|
+
1. blockers: issues that prevent crawlability, attribution, or answer extraction
|
|
102
|
+
2. high-impact GEO fixes: changes that improve answerability and citation probability
|
|
103
|
+
3. SEO support fixes: enhancements that reinforce discoverability and consistency
|
|
104
|
+
4. experiments: optional tests such as new page types, FAQ expansions, or comparison hubs
|
|
105
|
+
|
|
106
|
+
Prefer a short implementation backlog with owner-ready tasks over a long essay.
|
|
107
|
+
|
|
108
|
+
## Common Deliverables
|
|
109
|
+
|
|
110
|
+
### Audit deliverable
|
|
111
|
+
|
|
112
|
+
Return:
|
|
113
|
+
|
|
114
|
+
- current state summary
|
|
115
|
+
- strongest citation assets already present
|
|
116
|
+
- missing trust or structure signals
|
|
117
|
+
- prioritized fixes by effort and impact
|
|
118
|
+
|
|
119
|
+
Read [references/implementation-checklist.md](references/implementation-checklist.md) when you need a fuller checklist.
|
|
120
|
+
|
|
121
|
+
### Rewrite deliverable
|
|
122
|
+
|
|
123
|
+
For page rewrites:
|
|
124
|
+
|
|
125
|
+
- preserve factual accuracy
|
|
126
|
+
- tighten the opening into a direct answer
|
|
127
|
+
- add definitions, evidence, and question-led subheads
|
|
128
|
+
- surface author, reviewer, date, and source notes when possible
|
|
129
|
+
- keep the page useful for humans first
|
|
130
|
+
|
|
131
|
+
Read [references/geo-playbook.md](references/geo-playbook.md) when you need content patterns.
|
|
132
|
+
|
|
133
|
+
### Build deliverable
|
|
134
|
+
|
|
135
|
+
For new systems, propose:
|
|
136
|
+
|
|
137
|
+
- page templates by intent
|
|
138
|
+
- schema plan by page type
|
|
139
|
+
- evidence collection workflow
|
|
140
|
+
- editorial rules for freshness, citations, and entity consistency
|
|
141
|
+
- measurement plan tied to both AI-search and traditional SEO outcomes
|
|
142
|
+
|
|
143
|
+
## Execution Notes
|
|
144
|
+
|
|
145
|
+
When the user asks for a codebase implementation:
|
|
146
|
+
|
|
147
|
+
- inspect templates, layouts, metadata helpers, routing, and schema generation points
|
|
148
|
+
- modify reusable primitives instead of patching one page at a time when possible
|
|
149
|
+
- add examples for the highest-value page type first
|
|
150
|
+
|
|
151
|
+
When the user asks for strategic recommendations:
|
|
152
|
+
|
|
153
|
+
- anchor every recommendation to a page type, entity, or evidence gap
|
|
154
|
+
- avoid broad advice like "publish better content" without specifying the format and missing proof
|
|
155
|
+
|
|
156
|
+
When the user asks for the latest vendor behavior or policy:
|
|
157
|
+
|
|
158
|
+
- verify with current primary sources before making claims about Google AI Overviews, ChatGPT, Gemini, Claude, Perplexity, or schema support
|
|
159
|
+
|
|
160
|
+
## Bundled Resources
|
|
161
|
+
|
|
162
|
+
- `scripts/scan-geo-signals.mjs`: local scanner for crawlability, schema, answerability, and citation signals
|
|
163
|
+
- `references/geo-playbook.md`: content patterns and GEO-first page design heuristics
|
|
164
|
+
- `references/implementation-checklist.md`: technical, content, and measurement checklist
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# GEO Playbook
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- Answer-engine content model
|
|
6
|
+
- Page patterns that tend to travel into AI answers
|
|
7
|
+
- Evidence patterns
|
|
8
|
+
- Rewrite heuristics
|
|
9
|
+
- Anti-patterns
|
|
10
|
+
|
|
11
|
+
## Answer-engine content model
|
|
12
|
+
|
|
13
|
+
Design pages so a model can answer four questions fast:
|
|
14
|
+
|
|
15
|
+
1. What is this entity?
|
|
16
|
+
2. Why should this source be trusted?
|
|
17
|
+
3. What is the direct answer?
|
|
18
|
+
4. What evidence supports the answer?
|
|
19
|
+
|
|
20
|
+
Make those answers visible in the page structure, not buried in prose.
|
|
21
|
+
|
|
22
|
+
## Page patterns that tend to travel into AI answers
|
|
23
|
+
|
|
24
|
+
### Definition pages
|
|
25
|
+
|
|
26
|
+
Use for category, concept, feature, or industry terminology.
|
|
27
|
+
|
|
28
|
+
Include:
|
|
29
|
+
|
|
30
|
+
- one-sentence definition near the top
|
|
31
|
+
- who it is for
|
|
32
|
+
- how it differs from adjacent concepts
|
|
33
|
+
- short examples
|
|
34
|
+
- linked deeper pages for workflow, pricing, or implementation
|
|
35
|
+
|
|
36
|
+
### Comparison pages
|
|
37
|
+
|
|
38
|
+
Use for `x vs y`, alternatives, replacement decisions, and buyer evaluation.
|
|
39
|
+
|
|
40
|
+
Include:
|
|
41
|
+
|
|
42
|
+
- explicit comparison criteria
|
|
43
|
+
- table format when possible
|
|
44
|
+
- ideal fit by segment or use case
|
|
45
|
+
- known tradeoffs
|
|
46
|
+
- update date if the market changes quickly
|
|
47
|
+
|
|
48
|
+
### FAQ hubs
|
|
49
|
+
|
|
50
|
+
Use only when questions are real and distinct.
|
|
51
|
+
|
|
52
|
+
Include:
|
|
53
|
+
|
|
54
|
+
- one direct answer per question
|
|
55
|
+
- a short expansion paragraph
|
|
56
|
+
- links to the canonical page for deeper detail
|
|
57
|
+
|
|
58
|
+
Avoid manufacturing low-value questions just to increase count.
|
|
59
|
+
|
|
60
|
+
### Methodology and research pages
|
|
61
|
+
|
|
62
|
+
Use when the brand has original data or proprietary process knowledge.
|
|
63
|
+
|
|
64
|
+
Include:
|
|
65
|
+
|
|
66
|
+
- what was measured
|
|
67
|
+
- date range
|
|
68
|
+
- sample size or scope
|
|
69
|
+
- method limitations
|
|
70
|
+
- key findings in summary form
|
|
71
|
+
|
|
72
|
+
These pages often provide the strongest citation material.
|
|
73
|
+
|
|
74
|
+
### Local and service pages
|
|
75
|
+
|
|
76
|
+
Use for businesses with geographic presence or service areas.
|
|
77
|
+
|
|
78
|
+
Include:
|
|
79
|
+
|
|
80
|
+
- exact service entity and location
|
|
81
|
+
- operating area, hours, contact, and service constraints
|
|
82
|
+
- proof such as certifications, case studies, reviews, or before/after artifacts
|
|
83
|
+
- local business and organization identity signals
|
|
84
|
+
|
|
85
|
+
## Evidence patterns
|
|
86
|
+
|
|
87
|
+
Prefer evidence in this order:
|
|
88
|
+
|
|
89
|
+
1. first-party data or product truth
|
|
90
|
+
2. primary sources
|
|
91
|
+
3. authoritative secondary summaries
|
|
92
|
+
4. internal opinion clearly labeled as opinion
|
|
93
|
+
|
|
94
|
+
Useful evidence blocks:
|
|
95
|
+
|
|
96
|
+
- benchmark tables
|
|
97
|
+
- methodology notes
|
|
98
|
+
- screenshots with captions
|
|
99
|
+
- pricing details
|
|
100
|
+
- implementation steps
|
|
101
|
+
- quote blocks from identifiable experts
|
|
102
|
+
- dated release or policy notes
|
|
103
|
+
|
|
104
|
+
## Rewrite heuristics
|
|
105
|
+
|
|
106
|
+
When rewriting a page, apply this sequence:
|
|
107
|
+
|
|
108
|
+
1. Replace vague intros with a direct answer.
|
|
109
|
+
2. Name the entity precisely.
|
|
110
|
+
3. Add one distinctive fact, method, or constraint early.
|
|
111
|
+
4. Break out question-led subheads.
|
|
112
|
+
5. Add comparison, checklist, or table sections where helpful.
|
|
113
|
+
6. Add explicit sourcing for non-obvious claims.
|
|
114
|
+
7. End with next-step links to deeper pages.
|
|
115
|
+
|
|
116
|
+
Good opening pattern:
|
|
117
|
+
|
|
118
|
+
`[Entity] is [short definition]. It is best for [audience/use case]. The main tradeoff is [constraint].`
|
|
119
|
+
|
|
120
|
+
## Anti-patterns
|
|
121
|
+
|
|
122
|
+
Avoid:
|
|
123
|
+
|
|
124
|
+
- keyword-stuffed headings
|
|
125
|
+
- faceless articles with no author or owner
|
|
126
|
+
- listicles with no unique evidence
|
|
127
|
+
- copy that hides the answer behind long brand storytelling
|
|
128
|
+
- duplicated pages targeting tiny wording variations
|
|
129
|
+
- unsupported superlatives such as "best" or "most accurate"
|
|
130
|
+
- fake FAQ schema for questions not visible on the page
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# GEO Implementation Checklist
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- Technical foundations
|
|
6
|
+
- Content and entity layer
|
|
7
|
+
- Structured data and attribution
|
|
8
|
+
- Measurement
|
|
9
|
+
|
|
10
|
+
## Technical foundations
|
|
11
|
+
|
|
12
|
+
Check:
|
|
13
|
+
|
|
14
|
+
- canonical URLs exist and point to the preferred version
|
|
15
|
+
- robots rules do not block important pages
|
|
16
|
+
- sitemap or sitemap generation exists
|
|
17
|
+
- page titles and meta descriptions are defined at scale
|
|
18
|
+
- heading hierarchy is readable and specific
|
|
19
|
+
- internal links connect summaries to deeper canonical pages
|
|
20
|
+
- duplicate or near-duplicate routes are controlled
|
|
21
|
+
- important pages are server-rendered or otherwise accessible to crawlers
|
|
22
|
+
|
|
23
|
+
## Content and entity layer
|
|
24
|
+
|
|
25
|
+
Check:
|
|
26
|
+
|
|
27
|
+
- each target page has one primary entity or decision topic
|
|
28
|
+
- the direct answer appears in the first screenful
|
|
29
|
+
- claims are tied to facts, examples, or sources
|
|
30
|
+
- authorship or responsible organization is visible
|
|
31
|
+
- freshness is visible through dates, version notes, or review notes
|
|
32
|
+
- comparison, FAQ, glossary, or methodology sections exist when intent requires them
|
|
33
|
+
- the page contains at least one piece of first-party value competitors cannot easily copy
|
|
34
|
+
|
|
35
|
+
## Structured data and attribution
|
|
36
|
+
|
|
37
|
+
Use only the schema that matches the visible page content.
|
|
38
|
+
|
|
39
|
+
Common candidates:
|
|
40
|
+
|
|
41
|
+
- `Organization`
|
|
42
|
+
- `WebSite`
|
|
43
|
+
- `Person`
|
|
44
|
+
- `Article`
|
|
45
|
+
- `FAQPage`
|
|
46
|
+
- `HowTo`
|
|
47
|
+
- `Product`
|
|
48
|
+
- `BreadcrumbList`
|
|
49
|
+
- `LocalBusiness`
|
|
50
|
+
|
|
51
|
+
Also check:
|
|
52
|
+
|
|
53
|
+
- `sameAs` links for organization or person entities when appropriate
|
|
54
|
+
- author or publisher details on article-like content
|
|
55
|
+
- product attributes that reflect the actual offer
|
|
56
|
+
- breadcrumb markup aligned with site structure
|
|
57
|
+
|
|
58
|
+
## Measurement
|
|
59
|
+
|
|
60
|
+
Track both AI-search and classic SEO outcomes.
|
|
61
|
+
|
|
62
|
+
Suggested measures:
|
|
63
|
+
|
|
64
|
+
- impressions and clicks from traditional search
|
|
65
|
+
- branded and non-branded query coverage
|
|
66
|
+
- referral traffic from AI/search assistants when observable
|
|
67
|
+
- growth in pages that earn citations, mentions, or backlinks
|
|
68
|
+
- engagement on comparison, FAQ, glossary, and methodology pages
|
|
69
|
+
- faster content production for answer-focused templates
|
|
70
|
+
|
|
71
|
+
For implementation backlogs, tag each item with:
|
|
72
|
+
|
|
73
|
+
- impact: `blocker`, `high`, `medium`, `low`
|
|
74
|
+
- type: `technical`, `content`, `schema`, `measurement`
|
|
75
|
+
- owner: engineering, content, SEO, product marketing, or subject-matter expert
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TEXT_EXTENSIONS = new Set([
|
|
5
|
+
".astro",
|
|
6
|
+
".html",
|
|
7
|
+
".htm",
|
|
8
|
+
".js",
|
|
9
|
+
".jsx",
|
|
10
|
+
".liquid",
|
|
11
|
+
".md",
|
|
12
|
+
".mdx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".njk",
|
|
15
|
+
".php",
|
|
16
|
+
".svelte",
|
|
17
|
+
".ts",
|
|
18
|
+
".tsx",
|
|
19
|
+
".txt",
|
|
20
|
+
".vue"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const IGNORE_DIRS = new Set([
|
|
24
|
+
".git",
|
|
25
|
+
".hg",
|
|
26
|
+
".next",
|
|
27
|
+
".nuxt",
|
|
28
|
+
".output",
|
|
29
|
+
".svelte-kit",
|
|
30
|
+
"build",
|
|
31
|
+
"coverage",
|
|
32
|
+
"dist",
|
|
33
|
+
"node_modules",
|
|
34
|
+
"out",
|
|
35
|
+
"tmp",
|
|
36
|
+
"vendor"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const SIGNAL_PATTERNS = {
|
|
40
|
+
title: /<title\b|metadata\s*[:=]\s*\{|title\s*:\s*['"]/i,
|
|
41
|
+
meta_description: /name=["']description["']|description\s*:\s*["']/i,
|
|
42
|
+
canonical: /rel=["']canonical["']|canonical\s*:\s*["']|alternates\s*:\s*\{/i,
|
|
43
|
+
json_ld: /application\/ld\+json|schema\.org/i,
|
|
44
|
+
faq_schema: /@type["']?\s*[:=]\s*["']FAQPage["']|schema\.org\/FAQPage/i,
|
|
45
|
+
howto_schema: /@type["']?\s*[:=]\s*["']HowTo["']|schema\.org\/HowTo/i,
|
|
46
|
+
article_schema:
|
|
47
|
+
/@type["']?\s*[:=]\s*["'](Article|BlogPosting|NewsArticle)["']|schema\.org\/(Article|BlogPosting|NewsArticle)/i,
|
|
48
|
+
organization_schema:
|
|
49
|
+
/@type["']?\s*[:=]\s*["'](Organization|Corporation|LocalBusiness)["']|schema\.org\/(Organization|Corporation|LocalBusiness)/i,
|
|
50
|
+
product_schema:
|
|
51
|
+
/@type["']?\s*[:=]\s*["'](Product|SoftwareApplication)["']|schema\.org\/(Product|SoftwareApplication)/i,
|
|
52
|
+
breadcrumb_schema:
|
|
53
|
+
/@type["']?\s*[:=]\s*["']BreadcrumbList["']|schema\.org\/BreadcrumbList/i,
|
|
54
|
+
author_markers: /\bauthor\b|\breviewed by\b|\bpublisher\b|\bdatePublished\b|\bdateModified\b/i,
|
|
55
|
+
qa_headings: /^(#{1,6}\s+.+\?)|(<h[1-6][^>]*>.*\?.*<\/h[1-6]>)/im,
|
|
56
|
+
comparison_intent: /\b(vs\.?|versus|alternative to|alternatives to|compare)\b/i,
|
|
57
|
+
original_research: /\b(benchmark|survey|study|dataset|methodology|research|report)\b/i,
|
|
58
|
+
citations: /https?:\/\//i
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
async function collectFiles(root) {
|
|
62
|
+
const files = [];
|
|
63
|
+
const queue = [root];
|
|
64
|
+
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const current = queue.shift();
|
|
67
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const entryPath = path.join(current, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
73
|
+
queue.push(entryPath);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
79
|
+
if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
|
|
80
|
+
files.push(entryPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readText(filePath, maxFileSize) {
|
|
89
|
+
try {
|
|
90
|
+
const stat = await fs.stat(filePath);
|
|
91
|
+
if (stat.size > maxFileSize) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return await fs.readFile(filePath, "utf8");
|
|
95
|
+
} catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildRecommendations(summary) {
|
|
101
|
+
const recommendations = [];
|
|
102
|
+
const { signals, specialFiles } = summary;
|
|
103
|
+
|
|
104
|
+
if (specialFiles.robotsTxt.length === 0) {
|
|
105
|
+
recommendations.push("Add or verify robots.txt so crawlers and answer engines see clear crawl rules.");
|
|
106
|
+
}
|
|
107
|
+
if (specialFiles.sitemap.length === 0) {
|
|
108
|
+
recommendations.push("Add a sitemap or sitemap generator to expose canonical URLs at scale.");
|
|
109
|
+
}
|
|
110
|
+
if (signals.canonical.count === 0) {
|
|
111
|
+
recommendations.push("Implement canonical tags or metadata helpers to reduce duplicate URL ambiguity.");
|
|
112
|
+
}
|
|
113
|
+
if (signals.json_ld.count === 0) {
|
|
114
|
+
recommendations.push("Add visible, page-matching structured data to improve entity disambiguation and citation confidence.");
|
|
115
|
+
}
|
|
116
|
+
if (signals.author_markers.count === 0) {
|
|
117
|
+
recommendations.push("Expose authorship, reviewer, publisher, or update metadata on advice pages.");
|
|
118
|
+
}
|
|
119
|
+
if (signals.qa_headings.count === 0) {
|
|
120
|
+
recommendations.push("Introduce question-led headings or FAQ sections on pages with informational intent.");
|
|
121
|
+
}
|
|
122
|
+
if (signals.citations.count === 0) {
|
|
123
|
+
recommendations.push("Add source links for non-obvious claims, benchmarks, or market statements.");
|
|
124
|
+
}
|
|
125
|
+
if (signals.original_research.count === 0) {
|
|
126
|
+
recommendations.push("Create at least one methodology, benchmark, or first-party evidence page that competitors cannot easily copy.");
|
|
127
|
+
}
|
|
128
|
+
if (signals.comparison_intent.count === 0) {
|
|
129
|
+
recommendations.push("Consider comparison or alternatives pages if you compete in a crowded category.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return recommendations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function scanProject(rootInput, options = {}) {
|
|
136
|
+
const root = path.resolve(rootInput);
|
|
137
|
+
const maxFileSize = options.maxFileSize || 1_000_000;
|
|
138
|
+
const maxExamples = options.maxExamples || 5;
|
|
139
|
+
|
|
140
|
+
const stat = await fs.stat(root).catch(() => null);
|
|
141
|
+
if (!stat) {
|
|
142
|
+
throw new Error(`Path not found: ${root}`);
|
|
143
|
+
}
|
|
144
|
+
if (!stat.isDirectory()) {
|
|
145
|
+
throw new Error(`Path is not a directory: ${root}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const files = await collectFiles(root);
|
|
149
|
+
const counts = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, 0]));
|
|
150
|
+
const examples = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, []]));
|
|
151
|
+
const specialFiles = {
|
|
152
|
+
robotsTxt: [],
|
|
153
|
+
sitemap: [],
|
|
154
|
+
llmsTxt: []
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let filesScanned = 0;
|
|
158
|
+
|
|
159
|
+
for (const filePath of files) {
|
|
160
|
+
const relativePath = path.relative(root, filePath);
|
|
161
|
+
const baseName = path.basename(filePath).toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (baseName === "robots.txt") {
|
|
164
|
+
specialFiles.robotsTxt.push(relativePath);
|
|
165
|
+
}
|
|
166
|
+
if (baseName === "llms.txt") {
|
|
167
|
+
specialFiles.llmsTxt.push(relativePath);
|
|
168
|
+
}
|
|
169
|
+
if (baseName.includes("sitemap")) {
|
|
170
|
+
specialFiles.sitemap.push(relativePath);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const content = await readText(filePath, maxFileSize);
|
|
174
|
+
if (!content) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
filesScanned += 1;
|
|
179
|
+
|
|
180
|
+
for (const [signal, pattern] of Object.entries(SIGNAL_PATTERNS)) {
|
|
181
|
+
if (pattern.test(content)) {
|
|
182
|
+
counts[signal] += 1;
|
|
183
|
+
if (examples[signal].length < maxExamples) {
|
|
184
|
+
examples[signal].push(relativePath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const summary = {
|
|
191
|
+
root,
|
|
192
|
+
filesConsidered: files.length,
|
|
193
|
+
filesScanned,
|
|
194
|
+
specialFiles,
|
|
195
|
+
signals: Object.fromEntries(
|
|
196
|
+
Object.keys(SIGNAL_PATTERNS).map((signal) => [
|
|
197
|
+
signal,
|
|
198
|
+
{
|
|
199
|
+
count: counts[signal],
|
|
200
|
+
examples: examples[signal]
|
|
201
|
+
}
|
|
202
|
+
])
|
|
203
|
+
)
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
summary.recommendations = buildRecommendations(summary);
|
|
207
|
+
return summary;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderScanMarkdown(summary) {
|
|
211
|
+
const lines = [
|
|
212
|
+
"# GEO Signal Scan",
|
|
213
|
+
"",
|
|
214
|
+
`- Root: \`${summary.root}\``,
|
|
215
|
+
`- Files considered: \`${summary.filesConsidered}\``,
|
|
216
|
+
`- Files scanned as text: \`${summary.filesScanned}\``,
|
|
217
|
+
"",
|
|
218
|
+
"## Foundations",
|
|
219
|
+
"",
|
|
220
|
+
`- robots.txt: \`${summary.specialFiles.robotsTxt.join(", ") || "missing"}\``,
|
|
221
|
+
`- sitemap: \`${summary.specialFiles.sitemap.join(", ") || "missing"}\``,
|
|
222
|
+
`- llms.txt: \`${summary.specialFiles.llmsTxt.join(", ") || "not found"}\``,
|
|
223
|
+
"",
|
|
224
|
+
"## Signal Counts",
|
|
225
|
+
""
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const [signal, details] of Object.entries(summary.signals)) {
|
|
229
|
+
const exampleText = details.examples.join(", ") || "none";
|
|
230
|
+
lines.push(`- ${signal}: \`${details.count}\` example(s): \`${exampleText}\``);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
lines.push("", "## Recommended Next Moves", "");
|
|
234
|
+
|
|
235
|
+
if (summary.recommendations.length === 0) {
|
|
236
|
+
lines.push("- No obvious blockers detected by heuristics. Review page quality and evidence depth next.");
|
|
237
|
+
} else {
|
|
238
|
+
for (const recommendation of summary.recommendations) {
|
|
239
|
+
lines.push(`- ${recommendation}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `${lines.join("\n")}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const [target = ".", ...flags] = process.argv.slice(2);
|
|
247
|
+
const outputJson = flags.includes("--json");
|
|
248
|
+
const summary = await scanProject(target);
|
|
249
|
+
|
|
250
|
+
if (outputJson) {
|
|
251
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
252
|
+
} else {
|
|
253
|
+
process.stdout.write(renderScanMarkdown(summary));
|
|
254
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { installSkill } from "./install-skill.js";
|
|
2
|
+
import { getBundledSkillPath, getInstalledSkillPath, getSkillName, getSkillsDir } from "./paths.js";
|
|
3
|
+
import { renderScanMarkdown, scanProject } from "./scan.js";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
let cachedVersion;
|
|
9
|
+
|
|
10
|
+
async function getPackageVersion() {
|
|
11
|
+
if (cachedVersion) {
|
|
12
|
+
return cachedVersion;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
16
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
17
|
+
cachedVersion = packageJson.version;
|
|
18
|
+
return cachedVersion;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printHelp() {
|
|
22
|
+
process.stdout.write(
|
|
23
|
+
[
|
|
24
|
+
"geo-ai-search-optimization",
|
|
25
|
+
"",
|
|
26
|
+
"Usage:",
|
|
27
|
+
" geo-ai-search-optimization",
|
|
28
|
+
" geo-ai-search-optimization install",
|
|
29
|
+
" geo-ai-search-optimization where",
|
|
30
|
+
" geo-ai-search-optimization scan <project-path> [--json]",
|
|
31
|
+
" geo-ai-search-optimization version",
|
|
32
|
+
" geo-ai-search-optimization help",
|
|
33
|
+
"",
|
|
34
|
+
"Environment overrides:",
|
|
35
|
+
" CODEX_HOME",
|
|
36
|
+
" CODEX_SKILLS_DIR",
|
|
37
|
+
" GEO_SKILL_INSTALL_DIR",
|
|
38
|
+
" GEO_SKILL_SKIP_POSTINSTALL"
|
|
39
|
+
].join("\n") + "\n"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runCli(args = []) {
|
|
44
|
+
const [command = "install", ...rest] = args;
|
|
45
|
+
|
|
46
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
47
|
+
printHelp();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
52
|
+
process.stdout.write(`${await getPackageVersion()}\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === "install") {
|
|
57
|
+
await installSkill();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (command === "where") {
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
[
|
|
64
|
+
`skill: ${getSkillName()}`,
|
|
65
|
+
`bundled: ${getBundledSkillPath()}`,
|
|
66
|
+
`skillsDir: ${getSkillsDir()}`,
|
|
67
|
+
`installed: ${getInstalledSkillPath()}`
|
|
68
|
+
].join("\n") + "\n"
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (command === "scan") {
|
|
74
|
+
const projectPath = rest.find((value) => !value.startsWith("-"));
|
|
75
|
+
if (!projectPath) {
|
|
76
|
+
throw new Error("scan requires a project path");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const outputJson = rest.includes("--json");
|
|
80
|
+
const summary = await scanProject(projectPath);
|
|
81
|
+
if (outputJson) {
|
|
82
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
83
|
+
} else {
|
|
84
|
+
process.stdout.write(renderScanMarkdown(summary));
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`Unknown command: ${command}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
93
|
+
|
|
94
|
+
if (isDirectRun) {
|
|
95
|
+
try {
|
|
96
|
+
await runCli(process.argv.slice(2));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
process.stderr.write(`geo-ai-search-optimization: ${error.message}\n`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getBundledSkillPath, getInstalledSkillPath, getSkillName } from "./paths.js";
|
|
4
|
+
|
|
5
|
+
async function pathExists(targetPath) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.access(targetPath);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function installSkill(options = {}) {
|
|
15
|
+
const sourceDir = getBundledSkillPath();
|
|
16
|
+
const targetDir = options.targetDir || getInstalledSkillPath();
|
|
17
|
+
const silent = Boolean(options.silent);
|
|
18
|
+
|
|
19
|
+
if (!(await pathExists(sourceDir))) {
|
|
20
|
+
throw new Error(`Bundled skill folder not found: ${sourceDir}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
24
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
25
|
+
await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
|
|
26
|
+
|
|
27
|
+
const manifest = {
|
|
28
|
+
skill: getSkillName(),
|
|
29
|
+
installedAt: new Date().toISOString(),
|
|
30
|
+
installedFrom: sourceDir,
|
|
31
|
+
packageName: "geo-ai-search-optimization"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
await fs.writeFile(
|
|
35
|
+
path.join(targetDir, ".installed-by-npm.json"),
|
|
36
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
37
|
+
"utf8"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!silent) {
|
|
41
|
+
process.stdout.write(`Installed ${getSkillName()} to ${targetDir}\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
targetDir,
|
|
46
|
+
sourceDir,
|
|
47
|
+
skillName: getSkillName()
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const RESOURCE_SKILL_NAME = "geo-ai-search-optimization";
|
|
6
|
+
|
|
7
|
+
export function getPackageRoot() {
|
|
8
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getBundledSkillPath() {
|
|
12
|
+
return path.join(getPackageRoot(), "resources", RESOURCE_SKILL_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getCodexHome() {
|
|
16
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getSkillsDir() {
|
|
20
|
+
return (
|
|
21
|
+
process.env.GEO_SKILL_INSTALL_DIR ||
|
|
22
|
+
process.env.CODEX_SKILLS_DIR ||
|
|
23
|
+
path.join(getCodexHome(), "skills")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getInstalledSkillPath() {
|
|
28
|
+
return path.join(getSkillsDir(), RESOURCE_SKILL_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getSkillName() {
|
|
32
|
+
return RESOURCE_SKILL_NAME;
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { installSkill } from "./install-skill.js";
|
|
2
|
+
|
|
3
|
+
if (process.env.GEO_SKILL_SKIP_POSTINSTALL === "1") {
|
|
4
|
+
process.exit(0);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await installSkill({ silent: true });
|
|
9
|
+
process.stdout.write("geo-ai-search-optimization: bundled skill installed\n");
|
|
10
|
+
} catch (error) {
|
|
11
|
+
process.stderr.write(`geo-ai-search-optimization: postinstall skipped (${error.message})\n`);
|
|
12
|
+
}
|
package/src/scan.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TEXT_EXTENSIONS = new Set([
|
|
5
|
+
".astro",
|
|
6
|
+
".html",
|
|
7
|
+
".htm",
|
|
8
|
+
".js",
|
|
9
|
+
".jsx",
|
|
10
|
+
".liquid",
|
|
11
|
+
".md",
|
|
12
|
+
".mdx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".njk",
|
|
15
|
+
".php",
|
|
16
|
+
".svelte",
|
|
17
|
+
".ts",
|
|
18
|
+
".tsx",
|
|
19
|
+
".txt",
|
|
20
|
+
".vue"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const IGNORE_DIRS = new Set([
|
|
24
|
+
".git",
|
|
25
|
+
".hg",
|
|
26
|
+
".next",
|
|
27
|
+
".nuxt",
|
|
28
|
+
".output",
|
|
29
|
+
".svelte-kit",
|
|
30
|
+
"build",
|
|
31
|
+
"coverage",
|
|
32
|
+
"dist",
|
|
33
|
+
"node_modules",
|
|
34
|
+
"out",
|
|
35
|
+
"tmp",
|
|
36
|
+
"vendor"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const SIGNAL_PATTERNS = {
|
|
40
|
+
title: /<title\b|metadata\s*[:=]\s*\{|title\s*:\s*['"]/i,
|
|
41
|
+
meta_description: /name=["']description["']|description\s*:\s*["']/i,
|
|
42
|
+
canonical: /rel=["']canonical["']|canonical\s*:\s*["']|alternates\s*:\s*\{/i,
|
|
43
|
+
json_ld: /application\/ld\+json|schema\.org/i,
|
|
44
|
+
faq_schema: /@type["']?\s*[:=]\s*["']FAQPage["']|schema\.org\/FAQPage/i,
|
|
45
|
+
howto_schema: /@type["']?\s*[:=]\s*["']HowTo["']|schema\.org\/HowTo/i,
|
|
46
|
+
article_schema:
|
|
47
|
+
/@type["']?\s*[:=]\s*["'](Article|BlogPosting|NewsArticle)["']|schema\.org\/(Article|BlogPosting|NewsArticle)/i,
|
|
48
|
+
organization_schema:
|
|
49
|
+
/@type["']?\s*[:=]\s*["'](Organization|Corporation|LocalBusiness)["']|schema\.org\/(Organization|Corporation|LocalBusiness)/i,
|
|
50
|
+
product_schema:
|
|
51
|
+
/@type["']?\s*[:=]\s*["'](Product|SoftwareApplication)["']|schema\.org\/(Product|SoftwareApplication)/i,
|
|
52
|
+
breadcrumb_schema:
|
|
53
|
+
/@type["']?\s*[:=]\s*["']BreadcrumbList["']|schema\.org\/BreadcrumbList/i,
|
|
54
|
+
author_markers: /\bauthor\b|\breviewed by\b|\bpublisher\b|\bdatePublished\b|\bdateModified\b/i,
|
|
55
|
+
qa_headings: /^(#{1,6}\s+.+\?)|(<h[1-6][^>]*>.*\?.*<\/h[1-6]>)/im,
|
|
56
|
+
comparison_intent: /\b(vs\.?|versus|alternative to|alternatives to|compare)\b/i,
|
|
57
|
+
original_research: /\b(benchmark|survey|study|dataset|methodology|research|report)\b/i,
|
|
58
|
+
citations: /https?:\/\//i
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
async function collectFiles(root) {
|
|
62
|
+
const files = [];
|
|
63
|
+
const queue = [root];
|
|
64
|
+
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const current = queue.shift();
|
|
67
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const entryPath = path.join(current, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
73
|
+
queue.push(entryPath);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
79
|
+
if (TEXT_EXTENSIONS.has(extension) || entry.name === "robots.txt" || entry.name === "llms.txt") {
|
|
80
|
+
files.push(entryPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readText(filePath, maxFileSize) {
|
|
89
|
+
try {
|
|
90
|
+
const stat = await fs.stat(filePath);
|
|
91
|
+
if (stat.size > maxFileSize) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return await fs.readFile(filePath, "utf8");
|
|
95
|
+
} catch {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildRecommendations(summary) {
|
|
101
|
+
const recommendations = [];
|
|
102
|
+
const { signals, specialFiles } = summary;
|
|
103
|
+
|
|
104
|
+
if (specialFiles.robotsTxt.length === 0) {
|
|
105
|
+
recommendations.push("Add or verify robots.txt so crawlers and answer engines see clear crawl rules.");
|
|
106
|
+
}
|
|
107
|
+
if (specialFiles.sitemap.length === 0) {
|
|
108
|
+
recommendations.push("Add a sitemap or sitemap generator to expose canonical URLs at scale.");
|
|
109
|
+
}
|
|
110
|
+
if (signals.canonical.count === 0) {
|
|
111
|
+
recommendations.push("Implement canonical tags or metadata helpers to reduce duplicate URL ambiguity.");
|
|
112
|
+
}
|
|
113
|
+
if (signals.json_ld.count === 0) {
|
|
114
|
+
recommendations.push("Add visible, page-matching structured data to improve entity disambiguation and citation confidence.");
|
|
115
|
+
}
|
|
116
|
+
if (signals.author_markers.count === 0) {
|
|
117
|
+
recommendations.push("Expose authorship, reviewer, publisher, or update metadata on advice pages.");
|
|
118
|
+
}
|
|
119
|
+
if (signals.qa_headings.count === 0) {
|
|
120
|
+
recommendations.push("Introduce question-led headings or FAQ sections on pages with informational intent.");
|
|
121
|
+
}
|
|
122
|
+
if (signals.citations.count === 0) {
|
|
123
|
+
recommendations.push("Add source links for non-obvious claims, benchmarks, or market statements.");
|
|
124
|
+
}
|
|
125
|
+
if (signals.original_research.count === 0) {
|
|
126
|
+
recommendations.push("Create at least one methodology, benchmark, or first-party evidence page that competitors cannot easily copy.");
|
|
127
|
+
}
|
|
128
|
+
if (signals.comparison_intent.count === 0) {
|
|
129
|
+
recommendations.push("Consider comparison or alternatives pages if you compete in a crowded category.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return recommendations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function scanProject(rootInput, options = {}) {
|
|
136
|
+
const root = path.resolve(rootInput);
|
|
137
|
+
const maxFileSize = options.maxFileSize || 1_000_000;
|
|
138
|
+
const maxExamples = options.maxExamples || 5;
|
|
139
|
+
|
|
140
|
+
const stat = await fs.stat(root).catch(() => null);
|
|
141
|
+
if (!stat) {
|
|
142
|
+
throw new Error(`Path not found: ${root}`);
|
|
143
|
+
}
|
|
144
|
+
if (!stat.isDirectory()) {
|
|
145
|
+
throw new Error(`Path is not a directory: ${root}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const files = await collectFiles(root);
|
|
149
|
+
const counts = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, 0]));
|
|
150
|
+
const examples = Object.fromEntries(Object.keys(SIGNAL_PATTERNS).map((key) => [key, []]));
|
|
151
|
+
const specialFiles = {
|
|
152
|
+
robotsTxt: [],
|
|
153
|
+
sitemap: [],
|
|
154
|
+
llmsTxt: []
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let filesScanned = 0;
|
|
158
|
+
|
|
159
|
+
for (const filePath of files) {
|
|
160
|
+
const relativePath = path.relative(root, filePath);
|
|
161
|
+
const baseName = path.basename(filePath).toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (baseName === "robots.txt") {
|
|
164
|
+
specialFiles.robotsTxt.push(relativePath);
|
|
165
|
+
}
|
|
166
|
+
if (baseName === "llms.txt") {
|
|
167
|
+
specialFiles.llmsTxt.push(relativePath);
|
|
168
|
+
}
|
|
169
|
+
if (baseName.includes("sitemap")) {
|
|
170
|
+
specialFiles.sitemap.push(relativePath);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const content = await readText(filePath, maxFileSize);
|
|
174
|
+
if (!content) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
filesScanned += 1;
|
|
179
|
+
|
|
180
|
+
for (const [signal, pattern] of Object.entries(SIGNAL_PATTERNS)) {
|
|
181
|
+
if (pattern.test(content)) {
|
|
182
|
+
counts[signal] += 1;
|
|
183
|
+
if (examples[signal].length < maxExamples) {
|
|
184
|
+
examples[signal].push(relativePath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const summary = {
|
|
191
|
+
root,
|
|
192
|
+
filesConsidered: files.length,
|
|
193
|
+
filesScanned,
|
|
194
|
+
specialFiles,
|
|
195
|
+
signals: Object.fromEntries(
|
|
196
|
+
Object.keys(SIGNAL_PATTERNS).map((signal) => [
|
|
197
|
+
signal,
|
|
198
|
+
{
|
|
199
|
+
count: counts[signal],
|
|
200
|
+
examples: examples[signal]
|
|
201
|
+
}
|
|
202
|
+
])
|
|
203
|
+
)
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
summary.recommendations = buildRecommendations(summary);
|
|
207
|
+
return summary;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function renderScanMarkdown(summary) {
|
|
211
|
+
const lines = [
|
|
212
|
+
"# GEO Signal Scan",
|
|
213
|
+
"",
|
|
214
|
+
`- Root: \`${summary.root}\``,
|
|
215
|
+
`- Files considered: \`${summary.filesConsidered}\``,
|
|
216
|
+
`- Files scanned as text: \`${summary.filesScanned}\``,
|
|
217
|
+
"",
|
|
218
|
+
"## Foundations",
|
|
219
|
+
"",
|
|
220
|
+
`- robots.txt: \`${summary.specialFiles.robotsTxt.join(", ") || "missing"}\``,
|
|
221
|
+
`- sitemap: \`${summary.specialFiles.sitemap.join(", ") || "missing"}\``,
|
|
222
|
+
`- llms.txt: \`${summary.specialFiles.llmsTxt.join(", ") || "not found"}\``,
|
|
223
|
+
"",
|
|
224
|
+
"## Signal Counts",
|
|
225
|
+
""
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const [signal, details] of Object.entries(summary.signals)) {
|
|
229
|
+
const exampleText = details.examples.join(", ") || "none";
|
|
230
|
+
lines.push(`- ${signal}: \`${details.count}\` example(s): \`${exampleText}\``);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
lines.push("", "## Recommended Next Moves", "");
|
|
234
|
+
|
|
235
|
+
if (summary.recommendations.length === 0) {
|
|
236
|
+
lines.push("- No obvious blockers detected by heuristics. Review page quality and evidence depth next.");
|
|
237
|
+
} else {
|
|
238
|
+
for (const recommendation of summary.recommendations) {
|
|
239
|
+
lines.push(`- ${recommendation}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `${lines.join("\n")}\n`;
|
|
244
|
+
}
|