seomd-cli 1.2.0 → 1.4.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 +9 -2
- package/package.json +1 -2
- package/src/commands/analyze.js +10 -10
- package/src/commands/init.js +13 -26
- package/src/commands/status.js +19 -7
- package/src/commands/sync.js +6 -6
- package/src/generators/directory.js +33 -65
- package/src/templates/blog/SEO.md +0 -115
- package/src/templates/ecommerce/SEO.md +0 -115
- package/src/templates/local/SEO.md +0 -115
- package/src/templates/marketplace/SEO.md +0 -115
- package/src/templates/saas/SEO.md +0 -115
- package/src/utils/writeback.js +44 -52
|
@@ -4,14 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
### spec v1.0 | <https://seomd.dev>
|
|
6
6
|
|
|
7
|
-
#### generated: {{date}}
|
|
8
|
-
|
|
9
|
-
## FIELD OWNERSHIP
|
|
10
|
-
|
|
11
|
-
### no prefix = founder declares (you own this)
|
|
12
|
-
|
|
13
|
-
### _analysis: = platform writes back (do not edit manually)
|
|
14
|
-
|
|
15
7
|
## Site
|
|
16
8
|
|
|
17
9
|
site:
|
|
@@ -36,8 +28,6 @@ identity:
|
|
|
36
28
|
|
|
37
29
|
keywords:
|
|
38
30
|
|
|
39
|
-
### FOUNDER DECLARES
|
|
40
|
-
|
|
41
31
|
primary: "{{primary_keyword}}"
|
|
42
32
|
secondary: [] # add your secondary keywords
|
|
43
33
|
negative: # terms that dilute your intent signal
|
|
@@ -52,30 +42,10 @@ keywords:
|
|
|
52
42
|
long_tail: [] # add long-tail variations
|
|
53
43
|
seasonal: null # add seasonal terms if applicable
|
|
54
44
|
|
|
55
|
-
### PLATFORM WRITES BACK
|
|
56
|
-
|
|
57
|
-
_analysis:
|
|
58
|
-
source: null
|
|
59
|
-
primary_search_volume: null
|
|
60
|
-
primary_intent_type: null
|
|
61
|
-
primary_trend: null
|
|
62
|
-
recommended_secondary: []
|
|
63
|
-
negative_additions_suggested: []
|
|
64
|
-
last_analyzed: null
|
|
65
|
-
next_analysis: null
|
|
66
|
-
|
|
67
45
|
## Intent
|
|
68
46
|
|
|
69
47
|
intent:
|
|
70
48
|
|
|
71
|
-
### FOUNDER DECLARES
|
|
72
|
-
|
|
73
|
-
#### Add queries your buyers actually type into AI engines
|
|
74
|
-
|
|
75
|
-
#### Tip: think about what someone asks ChatGPT or Perplexity
|
|
76
|
-
|
|
77
|
-
#### when they are looking for a solution like yours
|
|
78
|
-
|
|
79
49
|
informational:
|
|
80
50
|
priority: medium
|
|
81
51
|
queries:
|
|
@@ -108,47 +78,11 @@ intent:
|
|
|
108
78
|
- "best {{primary_keyword}}"
|
|
109
79
|
- "top {{primary_keyword}} 2026"
|
|
110
80
|
|
|
111
|
-
### PLATFORM WRITES BACK
|
|
112
|
-
|
|
113
|
-
_analysis:
|
|
114
|
-
source: null
|
|
115
|
-
last_analyzed: null
|
|
116
|
-
next_analysis: null
|
|
117
|
-
informational:
|
|
118
|
-
citation_rate: null
|
|
119
|
-
top_cited_competitor: null
|
|
120
|
-
gap_score: null
|
|
121
|
-
trend: null
|
|
122
|
-
comparison:
|
|
123
|
-
citation_rate: null
|
|
124
|
-
top_cited_competitor: null
|
|
125
|
-
gap_score: null
|
|
126
|
-
trend: null
|
|
127
|
-
transactional:
|
|
128
|
-
citation_rate: null
|
|
129
|
-
top_cited_competitor: null
|
|
130
|
-
gap_score: null
|
|
131
|
-
trend: null
|
|
132
|
-
reputational:
|
|
133
|
-
citation_rate: null
|
|
134
|
-
top_cited_competitor: null
|
|
135
|
-
gap_score: null
|
|
136
|
-
trend: null
|
|
137
|
-
category:
|
|
138
|
-
citation_rate: null
|
|
139
|
-
top_cited_competitor: null
|
|
140
|
-
gap_score: null
|
|
141
|
-
trend: null
|
|
142
|
-
|
|
143
81
|
## Pages
|
|
144
82
|
|
|
145
83
|
pages:
|
|
146
84
|
site_type: saas
|
|
147
85
|
|
|
148
|
-
### FOUNDER DECLARES
|
|
149
|
-
|
|
150
|
-
#### status: live | draft | planned
|
|
151
|
-
|
|
152
86
|
required:
|
|
153
87
|
- id: homepage
|
|
154
88
|
url: /
|
|
@@ -210,21 +144,10 @@ pages:
|
|
|
210
144
|
status: planned
|
|
211
145
|
priority: 10
|
|
212
146
|
|
|
213
|
-
### PLATFORM WRITES BACK
|
|
214
|
-
|
|
215
|
-
_analysis:
|
|
216
|
-
source: null
|
|
217
|
-
last_analyzed: null
|
|
218
|
-
pages: []
|
|
219
|
-
missing_pages: []
|
|
220
|
-
build_order_recommendation: []
|
|
221
|
-
|
|
222
147
|
## Copy
|
|
223
148
|
|
|
224
149
|
copy:
|
|
225
150
|
|
|
226
|
-
### FOUNDER DECLARES
|
|
227
|
-
|
|
228
151
|
h1_contains_primary_keyword: true
|
|
229
152
|
meta_description_length: 150-160
|
|
230
153
|
meta_description_includes_cta: true
|
|
@@ -239,8 +162,6 @@ copy:
|
|
|
239
162
|
|
|
240
163
|
structure:
|
|
241
164
|
|
|
242
|
-
### FOUNDER DECLARES
|
|
243
|
-
|
|
244
165
|
answer_first: true # direct answer in first 50 words
|
|
245
166
|
faq_section_required: true # on all key pages
|
|
246
167
|
faq_minimum_questions: 6
|
|
@@ -253,8 +174,6 @@ structure:
|
|
|
253
174
|
|
|
254
175
|
authority:
|
|
255
176
|
|
|
256
|
-
### FOUNDER DECLARES
|
|
257
|
-
|
|
258
177
|
cite_sources: true
|
|
259
178
|
expert_quotes: false # set true when you have quotes
|
|
260
179
|
eeat_signals:
|
|
@@ -267,8 +186,6 @@ authority:
|
|
|
267
186
|
|
|
268
187
|
schema:
|
|
269
188
|
|
|
270
|
-
### FOUNDER DECLARES
|
|
271
|
-
|
|
272
189
|
types:
|
|
273
190
|
- SoftwareApplication
|
|
274
191
|
- Organization
|
|
@@ -282,8 +199,6 @@ schema:
|
|
|
282
199
|
|
|
283
200
|
crawl:
|
|
284
201
|
|
|
285
|
-
### FOUNDER DECLARES
|
|
286
|
-
|
|
287
202
|
sitemap: /sitemap.xml
|
|
288
203
|
robots_txt: /robots.txt
|
|
289
204
|
allow_ai_bots: true
|
|
@@ -306,8 +221,6 @@ crawl:
|
|
|
306
221
|
|
|
307
222
|
performance:
|
|
308
223
|
|
|
309
|
-
### FOUNDER DECLARES
|
|
310
|
-
|
|
311
224
|
lcp: 2.5s
|
|
312
225
|
cls: 0.1
|
|
313
226
|
fid: 100ms
|
|
@@ -318,10 +231,6 @@ performance:
|
|
|
318
231
|
|
|
319
232
|
aeo:
|
|
320
233
|
|
|
321
|
-
### FOUNDER DECLARES
|
|
322
|
-
|
|
323
|
-
### AI Engine Optimization rules
|
|
324
|
-
|
|
325
234
|
answer_first_format: true
|
|
326
235
|
faq_on_all_key_pages: true
|
|
327
236
|
structured_data_priority: high
|
|
@@ -329,27 +238,10 @@ aeo:
|
|
|
329
238
|
competitors_to_monitor:
|
|
330
239
|
{{competitors_to_monitor}}
|
|
331
240
|
|
|
332
|
-
### PLATFORM WRITES BACK
|
|
333
|
-
|
|
334
|
-
_analysis:
|
|
335
|
-
source: null
|
|
336
|
-
overall_citation_rate: null
|
|
337
|
-
overall_gap_score: null
|
|
338
|
-
engines_tracked:
|
|
339
|
-
- chatgpt
|
|
340
|
-
- perplexity
|
|
341
|
-
- claude
|
|
342
|
-
- gemini
|
|
343
|
-
- grok
|
|
344
|
-
last_analyzed: null
|
|
345
|
-
next_analysis: null
|
|
346
|
-
|
|
347
241
|
## Monitoring
|
|
348
242
|
|
|
349
243
|
monitoring:
|
|
350
244
|
|
|
351
|
-
### FOUNDER DECLARES
|
|
352
|
-
|
|
353
245
|
sync_schedule: monthly # monthly | weekly | on_demand
|
|
354
246
|
auto_commit: false # platform commits directly to repo
|
|
355
247
|
pr_mode: true # open PR instead of direct commit
|
|
@@ -359,14 +251,7 @@ monitoring:
|
|
|
359
251
|
|
|
360
252
|
## Platform Connection
|
|
361
253
|
|
|
362
|
-
### Connect at <https://seomd.dev/connect>
|
|
363
|
-
|
|
364
|
-
### Add SEOMD_API_KEY to your .env file
|
|
365
|
-
|
|
366
|
-
### Never commit your API key to version control
|
|
367
|
-
|
|
368
254
|
platform:
|
|
369
255
|
provider: null # foxcite | manual | ahrefs | semrush
|
|
370
256
|
project_id: null
|
|
371
257
|
|
|
372
|
-
### api_key: loaded from SEOMD_API_KEY environment variable
|
package/src/utils/writeback.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
|
-
import { writeSeoMd } from './parser.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Writes platform analysis blocks into the .seo/STATUS.yml file.
|
|
8
7
|
*
|
|
9
|
-
* @param {YAML.Document} doc - The YAML Document of SEO.md
|
|
8
|
+
* @param {YAML.Document} doc - The YAML Document of SEO.md (deprecated, but kept for signature compatibility)
|
|
10
9
|
* @param {any} response - The API response from analyze/sync
|
|
11
10
|
* @param {string} cwd - Current working directory
|
|
12
11
|
*/
|
|
@@ -14,61 +13,60 @@ export async function writeAnalysisToSeoMd(doc, response, cwd) {
|
|
|
14
13
|
const lastAnalyzed = response.aeo_analysis.last_analyzed;
|
|
15
14
|
const nextAnalysis = response.aeo_analysis.next_analysis;
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
const statusData = {
|
|
17
|
+
source: 'foxcite',
|
|
18
|
+
last_analyzed: lastAnalyzed,
|
|
19
|
+
next_analysis: nextAnalysis,
|
|
20
|
+
aeo: {
|
|
21
|
+
overall_citation_rate: response.aeo_analysis.overall_citation_rate,
|
|
22
|
+
overall_gap_score: response.aeo_analysis.overall_gap_score,
|
|
23
|
+
engines_tracked: response.aeo_analysis.engines_tracked,
|
|
24
|
+
},
|
|
25
|
+
intent: {},
|
|
26
|
+
pages: {
|
|
27
|
+
pages: response.page_analysis.map(p => ({
|
|
28
|
+
id: p.id,
|
|
29
|
+
url: p.url,
|
|
30
|
+
citation_rate: p.citation_rate,
|
|
31
|
+
gap_score: p.gap_score,
|
|
32
|
+
top_cited_competitor: p.top_cited_competitor
|
|
33
|
+
})),
|
|
34
|
+
missing_pages: [],
|
|
35
|
+
build_order_recommendation: []
|
|
36
|
+
},
|
|
37
|
+
keywords: {}
|
|
38
|
+
};
|
|
29
39
|
|
|
30
40
|
const categories = ['informational', 'comparison', 'transactional', 'reputational', 'category'];
|
|
31
41
|
for (const cat of categories) {
|
|
32
42
|
if (response.intent_analysis && response.intent_analysis[cat]) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
statusData.intent[cat] = {
|
|
44
|
+
citation_rate: response.intent_analysis[cat].citation_rate,
|
|
45
|
+
top_cited_competitor: response.intent_analysis[cat].top_cited_competitor,
|
|
46
|
+
gap_score: response.intent_analysis[cat].gap_score,
|
|
47
|
+
trend: response.intent_analysis[cat].trend || 'stable'
|
|
48
|
+
};
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
url: p.url,
|
|
47
|
-
citation_rate: p.citation_rate,
|
|
48
|
-
gap_score: p.gap_score,
|
|
49
|
-
top_cited_competitor: p.top_cited_competitor
|
|
50
|
-
}));
|
|
51
|
-
doc.setIn(['pages', '_analysis', 'pages'], pageSummaries);
|
|
52
|
-
doc.setIn(['pages', '_analysis', 'missing_pages'], []);
|
|
53
|
-
doc.setIn(['pages', '_analysis', 'build_order_recommendation'], []);
|
|
54
|
-
|
|
55
|
-
// 4. Update Keywords analysis
|
|
56
|
-
doc.setIn(['keywords', '_analysis', 'source'], 'foxcite');
|
|
57
|
-
doc.setIn(['keywords', '_analysis', 'last_analyzed'], lastAnalyzed);
|
|
58
|
-
doc.setIn(['keywords', '_analysis', 'next_analysis'], nextAnalysis);
|
|
59
|
-
|
|
60
|
-
// Write back to SEO.md
|
|
61
|
-
await writeSeoMd(doc, cwd);
|
|
52
|
+
const seomdDir = path.join(cwd, '.seo');
|
|
53
|
+
await fs.ensureDir(seomdDir);
|
|
54
|
+
const statusPath = path.join(seomdDir, 'STATUS.yml');
|
|
55
|
+
|
|
56
|
+
const header = `# .seo/STATUS.yml\n#\n# This file is auto-generated by your connected SEO.md platform.\n# Do not edit manually — run \`npx seomd analyze\` to update.\n# Run \`npx seomd sync\` to pull latest intelligence.\n\n`;
|
|
57
|
+
await fs.writeFile(statusPath, header + YAML.stringify(statusData), 'utf8');
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
|
-
* Generates and writes the
|
|
61
|
+
* Generates and writes the .seo/REVERSE.md file.
|
|
66
62
|
*
|
|
67
63
|
* @param {string} cwd - Current working directory
|
|
68
64
|
* @param {any} response - The API response from analyze/sync
|
|
69
65
|
*/
|
|
70
66
|
export async function writeReverseMd(cwd, response, defaultDomain = 'example.com', defaultBrand = 'My Brand') {
|
|
71
|
-
const
|
|
67
|
+
const seomdDir = path.join(cwd, '.seo');
|
|
68
|
+
await fs.ensureDir(seomdDir);
|
|
69
|
+
const reversePath = path.join(seomdDir, 'REVERSE.md');
|
|
72
70
|
|
|
73
71
|
const reversePages = response.page_analysis.map(p => ({
|
|
74
72
|
id: p.id,
|
|
@@ -97,26 +95,20 @@ export async function writeReverseMd(cwd, response, defaultDomain = 'example.com
|
|
|
97
95
|
pages: reversePages
|
|
98
96
|
};
|
|
99
97
|
|
|
100
|
-
const header = `#
|
|
101
|
-
#
|
|
102
|
-
# This file is generated by the foxcite platform.
|
|
103
|
-
# Do not edit manually — run \`npx seomd analyze\` to update.
|
|
104
|
-
# Run \`npx seomd sync\` to pull latest intelligence.
|
|
105
|
-
|
|
106
|
-
`;
|
|
98
|
+
const header = `# .seo/REVERSE.md\n#\n# This file is auto-generated by your connected SEO.md platform.\n# Do not edit manually — run \`npx seomd analyze\` to update.\n# Run \`npx seomd sync\` to pull latest intelligence.\n\n`;
|
|
107
99
|
|
|
108
100
|
const yamlContent = YAML.stringify(reverseDoc);
|
|
109
101
|
await fs.writeFile(reversePath, header + yamlContent, 'utf8');
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
/**
|
|
113
|
-
* Writes individual page playbooks into .
|
|
105
|
+
* Writes individual page playbooks into .seo/pages/{id}.md files.
|
|
114
106
|
*
|
|
115
107
|
* @param {string} cwd - Current working directory
|
|
116
108
|
* @param {any} response - The API response from analyze/sync
|
|
117
109
|
*/
|
|
118
110
|
export async function writePageAnalysis(cwd, response) {
|
|
119
|
-
const seomdDir = path.join(cwd, '.
|
|
111
|
+
const seomdDir = path.join(cwd, '.seo');
|
|
120
112
|
const pagesDir = path.join(seomdDir, 'pages');
|
|
121
113
|
|
|
122
114
|
await fs.ensureDir(pagesDir);
|