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.
@@ -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
@@ -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
- * Merges platform analysis blocks back into the SEO.md YAML document.
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
- // 1. Update AEO analysis
18
- doc.setIn(['aeo', '_analysis', 'source'], 'foxcite');
19
- doc.setIn(['aeo', '_analysis', 'overall_citation_rate'], response.aeo_analysis.overall_citation_rate);
20
- doc.setIn(['aeo', '_analysis', 'overall_gap_score'], response.aeo_analysis.overall_gap_score);
21
- doc.setIn(['aeo', '_analysis', 'engines_tracked'], response.aeo_analysis.engines_tracked);
22
- doc.setIn(['aeo', '_analysis', 'last_analyzed'], lastAnalyzed);
23
- doc.setIn(['aeo', '_analysis', 'next_analysis'], nextAnalysis);
24
-
25
- // 2. Update Intent analysis
26
- doc.setIn(['intent', '_analysis', 'source'], 'foxcite');
27
- doc.setIn(['intent', '_analysis', 'last_analyzed'], lastAnalyzed);
28
- doc.setIn(['intent', '_analysis', 'next_analysis'], nextAnalysis);
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
- doc.setIn(['intent', '_analysis', cat, 'citation_rate'], response.intent_analysis[cat].citation_rate);
34
- doc.setIn(['intent', '_analysis', cat, 'top_cited_competitor'], response.intent_analysis[cat].top_cited_competitor);
35
- doc.setIn(['intent', '_analysis', cat, 'gap_score'], response.intent_analysis[cat].gap_score);
36
- doc.setIn(['intent', '_analysis', cat, 'trend'], response.intent_analysis[cat].trend || 'stable');
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
- // 3. Update Pages analysis
41
- doc.setIn(['pages', '_analysis', 'source'], 'foxcite');
42
- doc.setIn(['pages', '_analysis', 'last_analyzed'], lastAnalyzed);
43
-
44
- const pageSummaries = response.page_analysis.map(p => ({
45
- id: p.id,
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 SEO.REVERSE.md file.
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 reversePath = path.join(cwd, 'SEO.REVERSE.md');
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 = `# SEO.REVERSE.md
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 .seomd/pages/{id}.md files.
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, '.seomd');
111
+ const seomdDir = path.join(cwd, '.seo');
120
112
  const pagesDir = path.join(seomdDir, 'pages');
121
113
 
122
114
  await fs.ensureDir(pagesDir);