seomd-cli 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/bin/seomd.js +54 -0
- package/package.json +46 -0
- package/src/commands/analyze.js +145 -0
- package/src/commands/init.js +160 -0
- package/src/commands/status.js +192 -0
- package/src/commands/sync.js +122 -0
- package/src/commands/validate.js +58 -0
- package/src/generators/directory.js +136 -0
- package/src/generators/reverse.js +62 -0
- package/src/generators/seomd.js +55 -0
- package/src/templates/blog/SEO.REVERSE.md +176 -0
- package/src/templates/blog/SEO.md +348 -0
- package/src/templates/ecommerce/SEO.REVERSE.md +199 -0
- package/src/templates/ecommerce/SEO.md +354 -0
- package/src/templates/local/SEO.REVERSE.md +222 -0
- package/src/templates/local/SEO.md +360 -0
- package/src/templates/marketplace/SEO.REVERSE.md +222 -0
- package/src/templates/marketplace/SEO.md +360 -0
- package/src/templates/saas/SEO.REVERSE.md +268 -0
- package/src/templates/saas/SEO.md +372 -0
- package/src/utils/api-client.js +41 -0
- package/src/utils/constants.js +132 -0
- package/src/utils/parser.js +46 -0
- package/src/utils/writeback.js +130 -0
- package/src/validators/seomd.js +211 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# SEO.md
|
|
2
|
+
|
|
3
|
+
## {{brand}}
|
|
4
|
+
|
|
5
|
+
### spec v1.0 | <https://seomd.dev>
|
|
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
|
+
## Site
|
|
16
|
+
|
|
17
|
+
site:
|
|
18
|
+
type: saas
|
|
19
|
+
domain: {{domain}}
|
|
20
|
+
canonical: https://{{domain}}
|
|
21
|
+
locale: en-US
|
|
22
|
+
launched: null # YYYY-MM-DD
|
|
23
|
+
|
|
24
|
+
## Identity
|
|
25
|
+
|
|
26
|
+
identity:
|
|
27
|
+
brand: "{{brand}}"
|
|
28
|
+
tagline: null
|
|
29
|
+
social:
|
|
30
|
+
twitter: null
|
|
31
|
+
linkedin: null
|
|
32
|
+
github: null
|
|
33
|
+
schema_org_type: SoftwareApplication
|
|
34
|
+
|
|
35
|
+
## Keywords
|
|
36
|
+
|
|
37
|
+
keywords:
|
|
38
|
+
|
|
39
|
+
### FOUNDER DECLARES
|
|
40
|
+
|
|
41
|
+
primary: "{{primary_keyword}}"
|
|
42
|
+
secondary: [] # add your secondary keywords
|
|
43
|
+
negative: # terms that dilute your intent signal
|
|
44
|
+
- "free"
|
|
45
|
+
- "tutorial"
|
|
46
|
+
- "how to"
|
|
47
|
+
competitor_terms:
|
|
48
|
+
{{competitor_terms}}
|
|
49
|
+
category_terms: # unbranded category queries
|
|
50
|
+
- "best {{primary_keyword}}"
|
|
51
|
+
- "top {{primary_keyword}} tools"
|
|
52
|
+
long_tail: [] # add long-tail variations
|
|
53
|
+
seasonal: null # add seasonal terms if applicable
|
|
54
|
+
|
|
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
|
+
## Intent
|
|
68
|
+
|
|
69
|
+
intent:
|
|
70
|
+
|
|
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
|
+
informational:
|
|
80
|
+
priority: medium
|
|
81
|
+
queries:
|
|
82
|
+
- "what is {{primary_keyword}}"
|
|
83
|
+
- "how does {{primary_keyword}} work"
|
|
84
|
+
|
|
85
|
+
comparison:
|
|
86
|
+
priority: high
|
|
87
|
+
queries:
|
|
88
|
+
{{competitors_comparison_queries}}
|
|
89
|
+
- "best {{primary_keyword}} for startups"
|
|
90
|
+
|
|
91
|
+
transactional:
|
|
92
|
+
priority: high
|
|
93
|
+
queries:
|
|
94
|
+
- "is {{brand_lower}} worth it"
|
|
95
|
+
- "should I use {{brand_lower}}"
|
|
96
|
+
- "{{brand_lower}} pricing"
|
|
97
|
+
|
|
98
|
+
reputational:
|
|
99
|
+
priority: medium
|
|
100
|
+
queries:
|
|
101
|
+
- "{{brand_lower}} reviews"
|
|
102
|
+
- "is {{brand_lower}} legit"
|
|
103
|
+
- "{{brand_lower}} problems"
|
|
104
|
+
|
|
105
|
+
category:
|
|
106
|
+
priority: critical
|
|
107
|
+
queries:
|
|
108
|
+
- "best {{primary_keyword}}"
|
|
109
|
+
- "top {{primary_keyword}} 2026"
|
|
110
|
+
|
|
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
|
+
## Pages
|
|
144
|
+
|
|
145
|
+
pages:
|
|
146
|
+
site_type: saas
|
|
147
|
+
|
|
148
|
+
### FOUNDER DECLARES
|
|
149
|
+
|
|
150
|
+
#### status: live | draft | planned
|
|
151
|
+
|
|
152
|
+
required:
|
|
153
|
+
- id: homepage
|
|
154
|
+
url: /
|
|
155
|
+
primary_keyword: null
|
|
156
|
+
status: planned
|
|
157
|
+
priority: 1
|
|
158
|
+
|
|
159
|
+
- id: features
|
|
160
|
+
url: /features
|
|
161
|
+
primary_keyword: null
|
|
162
|
+
status: planned
|
|
163
|
+
priority: 2
|
|
164
|
+
|
|
165
|
+
- id: pricing
|
|
166
|
+
url: /pricing
|
|
167
|
+
primary_keyword: null
|
|
168
|
+
status: planned
|
|
169
|
+
priority: 3
|
|
170
|
+
|
|
171
|
+
- id: comparison
|
|
172
|
+
url: /vs/[competitor]
|
|
173
|
+
primary_keyword: null
|
|
174
|
+
status: planned
|
|
175
|
+
priority: 4
|
|
176
|
+
|
|
177
|
+
- id: alternatives
|
|
178
|
+
url: /alternatives/[competitor]
|
|
179
|
+
primary_keyword: null
|
|
180
|
+
status: planned
|
|
181
|
+
priority: 5
|
|
182
|
+
|
|
183
|
+
- id: use-cases
|
|
184
|
+
url: /for/[segment]
|
|
185
|
+
primary_keyword: null
|
|
186
|
+
status: planned
|
|
187
|
+
priority: 6
|
|
188
|
+
|
|
189
|
+
- id: integrations
|
|
190
|
+
url: /integrations
|
|
191
|
+
primary_keyword: null
|
|
192
|
+
status: planned
|
|
193
|
+
priority: 7
|
|
194
|
+
|
|
195
|
+
- id: changelog
|
|
196
|
+
url: /changelog
|
|
197
|
+
primary_keyword: null
|
|
198
|
+
status: planned
|
|
199
|
+
priority: 8
|
|
200
|
+
|
|
201
|
+
- id: docs
|
|
202
|
+
url: /docs
|
|
203
|
+
primary_keyword: null
|
|
204
|
+
status: planned
|
|
205
|
+
priority: 9
|
|
206
|
+
|
|
207
|
+
- id: blog
|
|
208
|
+
url: /blog
|
|
209
|
+
primary_keyword: null
|
|
210
|
+
status: planned
|
|
211
|
+
priority: 10
|
|
212
|
+
|
|
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
|
+
## Copy
|
|
223
|
+
|
|
224
|
+
copy:
|
|
225
|
+
|
|
226
|
+
### FOUNDER DECLARES
|
|
227
|
+
|
|
228
|
+
h1_contains_primary_keyword: true
|
|
229
|
+
meta_description_length: 150-160
|
|
230
|
+
meta_description_includes_cta: true
|
|
231
|
+
min_word_count:
|
|
232
|
+
homepage: 800
|
|
233
|
+
feature_page: 600
|
|
234
|
+
blog_post: 1200
|
|
235
|
+
comparison_page: 1500
|
|
236
|
+
reading_level: 8 # grade level target
|
|
237
|
+
|
|
238
|
+
## Structure
|
|
239
|
+
|
|
240
|
+
structure:
|
|
241
|
+
|
|
242
|
+
### FOUNDER DECLARES
|
|
243
|
+
|
|
244
|
+
answer_first: true # direct answer in first 50 words
|
|
245
|
+
faq_section_required: true # on all key pages
|
|
246
|
+
faq_minimum_questions: 6
|
|
247
|
+
statistics_per_page: 2 # minimum data points with sources
|
|
248
|
+
citations_required: true # link to primary sources
|
|
249
|
+
short_paragraphs: true # max 3 sentences
|
|
250
|
+
heading_hierarchy: strict # H1 > H2 > H3, no skipping
|
|
251
|
+
|
|
252
|
+
## Authority
|
|
253
|
+
|
|
254
|
+
authority:
|
|
255
|
+
|
|
256
|
+
### FOUNDER DECLARES
|
|
257
|
+
|
|
258
|
+
cite_sources: true
|
|
259
|
+
expert_quotes: false # set true when you have quotes
|
|
260
|
+
eeat_signals:
|
|
261
|
+
experience: null # describe your experience signal
|
|
262
|
+
expertise: null # describe your expertise signal
|
|
263
|
+
authority: null # describe your authority signal
|
|
264
|
+
trust: null # describe your trust signal
|
|
265
|
+
|
|
266
|
+
## Schema
|
|
267
|
+
|
|
268
|
+
schema:
|
|
269
|
+
|
|
270
|
+
### FOUNDER DECLARES
|
|
271
|
+
|
|
272
|
+
types:
|
|
273
|
+
- SoftwareApplication
|
|
274
|
+
- Organization
|
|
275
|
+
- FAQPage
|
|
276
|
+
- WebPage
|
|
277
|
+
faq_schema: true
|
|
278
|
+
breadcrumb_schema: true
|
|
279
|
+
organization_schema: true
|
|
280
|
+
|
|
281
|
+
## Crawl
|
|
282
|
+
|
|
283
|
+
crawl:
|
|
284
|
+
|
|
285
|
+
### FOUNDER DECLARES
|
|
286
|
+
|
|
287
|
+
sitemap: /sitemap.xml
|
|
288
|
+
robots_txt: /robots.txt
|
|
289
|
+
allow_ai_bots: true
|
|
290
|
+
allowed_bots:
|
|
291
|
+
- Googlebot
|
|
292
|
+
- Bingbot
|
|
293
|
+
- PerplexityBot
|
|
294
|
+
- ChatGPT-User
|
|
295
|
+
- GPTBot
|
|
296
|
+
- ClaudeBot
|
|
297
|
+
- anthropic-ai
|
|
298
|
+
- cohere-ai
|
|
299
|
+
disallow:
|
|
300
|
+
- /admin
|
|
301
|
+
- /checkout
|
|
302
|
+
- /user/*
|
|
303
|
+
- /api/*
|
|
304
|
+
|
|
305
|
+
## Performance
|
|
306
|
+
|
|
307
|
+
performance:
|
|
308
|
+
|
|
309
|
+
### FOUNDER DECLARES
|
|
310
|
+
|
|
311
|
+
lcp: 2.5s
|
|
312
|
+
cls: 0.1
|
|
313
|
+
fid: 100ms
|
|
314
|
+
page_size: 500kb
|
|
315
|
+
ttfb: 800ms
|
|
316
|
+
|
|
317
|
+
## AEO
|
|
318
|
+
|
|
319
|
+
aeo:
|
|
320
|
+
|
|
321
|
+
### FOUNDER DECLARES
|
|
322
|
+
|
|
323
|
+
### AI Engine Optimization rules
|
|
324
|
+
|
|
325
|
+
answer_first_format: true
|
|
326
|
+
faq_on_all_key_pages: true
|
|
327
|
+
structured_data_priority: high
|
|
328
|
+
content_freshness_target: 30d # update key pages within 30 days
|
|
329
|
+
competitors_to_monitor:
|
|
330
|
+
{{competitors_to_monitor}}
|
|
331
|
+
|
|
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
|
+
## Monitoring
|
|
348
|
+
|
|
349
|
+
monitoring:
|
|
350
|
+
|
|
351
|
+
### FOUNDER DECLARES
|
|
352
|
+
|
|
353
|
+
sync_schedule: monthly # monthly | weekly | on_demand
|
|
354
|
+
auto_commit: false # platform commits directly to repo
|
|
355
|
+
pr_mode: true # open PR instead of direct commit
|
|
356
|
+
branch: main
|
|
357
|
+
alert_on_gap_score_above: 80 # alert when gap score exceeds threshold
|
|
358
|
+
alert_on_citation_drop: true # alert if citation rate drops
|
|
359
|
+
|
|
360
|
+
## Platform Connection
|
|
361
|
+
|
|
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
|
+
platform:
|
|
369
|
+
provider: null # foxcite | manual | ahrefs | semrush
|
|
370
|
+
project_id: null
|
|
371
|
+
|
|
372
|
+
### api_key: loaded from SEOMD_API_KEY environment variable
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
|
|
4
|
+
// Load .env configuration
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
const API_URL = process.env.SEOMD_API_URL || 'https://api.foxcite.com';
|
|
8
|
+
const API_KEY = process.env.SEOMD_API_KEY;
|
|
9
|
+
const PAYMENT_TOKEN = process.env.SEOMD_PAYMENT_TOKEN;
|
|
10
|
+
const SEOMD_DOMAIN = process.env.SEOMD_DOMAIN;
|
|
11
|
+
|
|
12
|
+
export const client = axios.create({
|
|
13
|
+
baseURL: API_URL,
|
|
14
|
+
timeout: 300000, // 5 minutes timeout for LLM audits
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...(API_KEY ? { 'Authorization': `Bearer ${API_KEY}` } : {}),
|
|
18
|
+
...(PAYMENT_TOKEN ? { 'x-payment-token': PAYMENT_TOKEN } : {}),
|
|
19
|
+
...(SEOMD_DOMAIN ? { 'x-seomd-domain': SEOMD_DOMAIN } : {})
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Interceptor for cleaner error feedback
|
|
24
|
+
client.interceptors.response.use(
|
|
25
|
+
(response) => response,
|
|
26
|
+
(error) => {
|
|
27
|
+
if (error.response) {
|
|
28
|
+
const status = error.response.status;
|
|
29
|
+
const detail = error.response.data?.detail || error.response.data?.message || error.message;
|
|
30
|
+
|
|
31
|
+
if (status === 401) {
|
|
32
|
+
return Promise.reject(new Error('Authentication failed: Invalid or missing API key (SEOMD_API_KEY).'));
|
|
33
|
+
}
|
|
34
|
+
if (status === 402) {
|
|
35
|
+
return Promise.reject(new Error(`Payment Required: Insufficient scan credits. ${detail}`));
|
|
36
|
+
}
|
|
37
|
+
return Promise.reject(new Error(`API Error (${status}): ${detail}`));
|
|
38
|
+
}
|
|
39
|
+
return Promise.reject(new Error(`Network Error: ${error.message}`));
|
|
40
|
+
}
|
|
41
|
+
);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export const SITE_TYPES = [
|
|
2
|
+
{ name: 'SaaS / Software', value: 'saas' },
|
|
3
|
+
{ name: 'Ecommerce', value: 'ecommerce' },
|
|
4
|
+
{ name: 'Local Business', value: 'local' },
|
|
5
|
+
{ name: 'Blog / Media', value: 'blog' },
|
|
6
|
+
{ name: 'Marketplace', value: 'marketplace' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const INTENT_CATEGORIES = [
|
|
10
|
+
'informational',
|
|
11
|
+
'comparison',
|
|
12
|
+
'transactional',
|
|
13
|
+
'reputational',
|
|
14
|
+
'category',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const INTENT_PRIORITIES = {
|
|
18
|
+
saas: {
|
|
19
|
+
informational: 'medium',
|
|
20
|
+
comparison: 'high',
|
|
21
|
+
transactional: 'high',
|
|
22
|
+
reputational: 'medium',
|
|
23
|
+
category: 'critical',
|
|
24
|
+
},
|
|
25
|
+
ecommerce: {
|
|
26
|
+
informational: 'low',
|
|
27
|
+
comparison: 'high',
|
|
28
|
+
transactional: 'critical',
|
|
29
|
+
reputational: 'high',
|
|
30
|
+
category: 'high',
|
|
31
|
+
},
|
|
32
|
+
local: {
|
|
33
|
+
informational: 'medium',
|
|
34
|
+
comparison: 'medium',
|
|
35
|
+
transactional: 'critical',
|
|
36
|
+
reputational: 'critical',
|
|
37
|
+
category: 'high',
|
|
38
|
+
},
|
|
39
|
+
blog: {
|
|
40
|
+
informational: 'critical',
|
|
41
|
+
comparison: 'medium',
|
|
42
|
+
transactional: 'low',
|
|
43
|
+
reputational: 'medium',
|
|
44
|
+
category: 'high',
|
|
45
|
+
},
|
|
46
|
+
marketplace: {
|
|
47
|
+
informational: 'medium',
|
|
48
|
+
comparison: 'high',
|
|
49
|
+
transactional: 'critical',
|
|
50
|
+
reputational: 'high',
|
|
51
|
+
category: 'high',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const REQUIRED_PAGES = {
|
|
56
|
+
saas: [
|
|
57
|
+
{ id: 'homepage', url: '/', priority: 1 },
|
|
58
|
+
{ id: 'features', url: '/features', priority: 2 },
|
|
59
|
+
{ id: 'pricing', url: '/pricing', priority: 3 },
|
|
60
|
+
{ id: 'comparison', url: '/vs/[competitor]', priority: 4 },
|
|
61
|
+
{ id: 'alternatives', url: '/alternatives/[competitor]', priority: 5 },
|
|
62
|
+
{ id: 'use-cases', url: '/for/[segment]', priority: 6 },
|
|
63
|
+
{ id: 'integrations', url: '/integrations', priority: 7 },
|
|
64
|
+
{ id: 'changelog', url: '/changelog', priority: 8 },
|
|
65
|
+
{ id: 'docs', url: '/docs', priority: 9 },
|
|
66
|
+
{ id: 'blog', url: '/blog', priority: 10 },
|
|
67
|
+
],
|
|
68
|
+
ecommerce: [
|
|
69
|
+
{ id: 'homepage', url: '/', priority: 1 },
|
|
70
|
+
{ id: 'category', url: '/[category]', priority: 2 },
|
|
71
|
+
{ id: 'product', url: '/products/[slug]', priority: 3 },
|
|
72
|
+
{ id: 'collection', url: '/collections/[slug]', priority: 4 },
|
|
73
|
+
{ id: 'reviews', url: '/reviews', priority: 5 },
|
|
74
|
+
{ id: 'cart', url: '/cart', priority: 6 },
|
|
75
|
+
{ id: 'checkout', url: '/checkout', priority: 7 },
|
|
76
|
+
],
|
|
77
|
+
local: [
|
|
78
|
+
{ id: 'homepage', url: '/', priority: 1 },
|
|
79
|
+
{ id: 'services', url: '/services', priority: 2 },
|
|
80
|
+
{ id: 'location', url: '/[city]', priority: 3 },
|
|
81
|
+
{ id: 'about', url: '/about', priority: 4 },
|
|
82
|
+
{ id: 'reviews', url: '/reviews', priority: 5 },
|
|
83
|
+
{ id: 'faq', url: '/faq', priority: 6 },
|
|
84
|
+
{ id: 'contact', url: '/contact', priority: 7 },
|
|
85
|
+
{ id: 'service-area', url: '/service-area', priority: 8 },
|
|
86
|
+
],
|
|
87
|
+
blog: [
|
|
88
|
+
{ id: 'homepage', url: '/', priority: 1 },
|
|
89
|
+
{ id: 'category', url: '/[category]', priority: 2 },
|
|
90
|
+
{ id: 'article', url: '/[category]/[slug]', priority: 3 },
|
|
91
|
+
{ id: 'author', url: '/author/[slug]', priority: 4 },
|
|
92
|
+
{ id: 'about', url: '/about', priority: 5 },
|
|
93
|
+
{ id: 'newsletter', url: '/newsletter', priority: 6 },
|
|
94
|
+
],
|
|
95
|
+
marketplace: [
|
|
96
|
+
{ id: 'homepage', url: '/', priority: 1 },
|
|
97
|
+
{ id: 'listing', url: '/listings/[slug]', priority: 2 },
|
|
98
|
+
{ id: 'category', url: '/[category]', priority: 3 },
|
|
99
|
+
{ id: 'seller-profile', url: '/sellers/[slug]', priority: 4 },
|
|
100
|
+
{ id: 'search-results', url: '/search', priority: 5 },
|
|
101
|
+
{ id: 'how-it-works', url: '/how-it-works', priority: 6 },
|
|
102
|
+
{ id: 'pricing', url: '/pricing', priority: 7 },
|
|
103
|
+
{ id: 'trust-safety', url: '/trust', priority: 8 },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const SCHEMA_TYPES = {
|
|
108
|
+
saas: ['SoftwareApplication', 'Organization', 'FAQPage', 'WebPage'],
|
|
109
|
+
ecommerce: ['Product', 'Organization', 'FAQPage', 'BreadcrumbList'],
|
|
110
|
+
local: ['LocalBusiness', 'FAQPage', 'Review', 'GeoCoordinates'],
|
|
111
|
+
blog: ['Article', 'Person', 'FAQPage', 'BreadcrumbList'],
|
|
112
|
+
marketplace: ['WebSite', 'Organization', 'FAQPage', 'Product'],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const PERFORMANCE_THRESHOLDS = {
|
|
116
|
+
lcp: '2.5s',
|
|
117
|
+
cls: '0.1',
|
|
118
|
+
fid: '100ms',
|
|
119
|
+
page_size: '500kb',
|
|
120
|
+
time_to_first_byte: '800ms',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const AI_BOTS = [
|
|
124
|
+
'Googlebot',
|
|
125
|
+
'Bingbot',
|
|
126
|
+
'PerplexityBot',
|
|
127
|
+
'ChatGPT-User',
|
|
128
|
+
'GPTBot',
|
|
129
|
+
'ClaudeBot',
|
|
130
|
+
'anthropic-ai',
|
|
131
|
+
'cohere-ai',
|
|
132
|
+
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses the SEO.md file in the specified directory.
|
|
7
|
+
* Returns both the plain JavaScript object and the YAML Document (for comment preservation).
|
|
8
|
+
*
|
|
9
|
+
* @param {string} [cwd=process.cwd()] - Current working directory
|
|
10
|
+
* @returns {Promise<{doc: YAML.Document, data: any}>} Parsed SEO.md representation
|
|
11
|
+
*/
|
|
12
|
+
export async function parseSeoMd(cwd = process.cwd()) {
|
|
13
|
+
const seomdPath = path.join(cwd, 'SEO.md');
|
|
14
|
+
|
|
15
|
+
if (!(await fs.pathExists(seomdPath))) {
|
|
16
|
+
throw new Error(`SEO.md not found at ${seomdPath}. Run 'seomd init' first.`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content = await fs.readFile(seomdPath, 'utf8');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const doc = YAML.parseDocument(content);
|
|
23
|
+
const data = doc.toJS();
|
|
24
|
+
|
|
25
|
+
if (!data || typeof data !== 'object') {
|
|
26
|
+
throw new Error('SEO.md is empty or invalid YAML structure');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { doc, data };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new Error(`Failed to parse SEO.md: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Writes the YAML Document back to SEO.md in the specified directory.
|
|
37
|
+
*
|
|
38
|
+
* @param {YAML.Document} doc - The YAML Document to write
|
|
39
|
+
* @param {string} [cwd=process.cwd()] - Current working directory
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
export async function writeSeoMd(doc, cwd = process.cwd()) {
|
|
43
|
+
const seomdPath = path.join(cwd, 'SEO.md');
|
|
44
|
+
const content = doc.toString();
|
|
45
|
+
await fs.writeFile(seomdPath, content, 'utf8');
|
|
46
|
+
}
|