seomd-cli 1.1.2 → 1.3.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 +77 -9
- package/bin/seomd.js +37 -0
- package/package.json +8 -7
- package/src/commands/analyze.js +5 -6
- package/src/commands/init.js +41 -19
- package/src/commands/sync.js +2 -3
- package/src/generators/directory.js +66 -15
- package/src/utils/writeback.js +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,12 @@ SEO.md is a structured, version-controlled specification for describing your sit
|
|
|
36
36
|
npm install -g seomd-cli
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Or run without installing (zero-install):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx seomd-cli init
|
|
43
|
+
```
|
|
44
|
+
|
|
39
45
|
Verify:
|
|
40
46
|
|
|
41
47
|
```bash
|
|
@@ -52,6 +58,14 @@ Run in the root of your project:
|
|
|
52
58
|
seomd init
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
Or non-interactively with flags:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
seomd init -y --type local
|
|
65
|
+
# or
|
|
66
|
+
seomd init --brand "Acme" --domain acme.com --primary-keyword "local seo"
|
|
67
|
+
```
|
|
68
|
+
|
|
55
69
|
### 2) Validate
|
|
56
70
|
|
|
57
71
|
```bash
|
|
@@ -74,24 +88,57 @@ seomd status --json
|
|
|
74
88
|
|
|
75
89
|
## Configuration
|
|
76
90
|
|
|
77
|
-
Copy the
|
|
91
|
+
Copy `.env.example` from your platform provider docs, or create one with the vars you need:
|
|
92
|
+
|
|
93
|
+
Required for live audits:
|
|
78
94
|
|
|
79
95
|
```bash
|
|
80
|
-
|
|
96
|
+
SEOMD_API_URL=
|
|
97
|
+
SEOMD_API_KEY=your_key_here
|
|
81
98
|
```
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
Optional:
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
```bash
|
|
103
|
+
SEOMD_PAYMENT_TOKEN= # x402 pay-per-scan token
|
|
104
|
+
SEOMD_DOMAIN= # override domain header
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> If you don't have a platform key yet, `seomd init` still works without `.env`.
|
|
89
108
|
|
|
90
109
|
## Commands
|
|
91
110
|
|
|
92
111
|
### `seomd init`
|
|
93
112
|
|
|
94
|
-
Scaffolds `SEO.md`, `SEO.REVERSE.md`, and the `.
|
|
113
|
+
Scaffolds `SEO.md`, `SEO.REVERSE.md`, and the `.seo/` intelligence directory.
|
|
114
|
+
|
|
115
|
+
**Usage:**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
seomd init # interactive 5-question flow
|
|
119
|
+
seomd init -y --type local # skip prompts, use defaults
|
|
120
|
+
seomd init --brand "Acme" --domain acme.com # non-interactive with partial flags
|
|
121
|
+
seomd init --type saas --brand "MyApp" --domain myapp.com --primary-keyword "billing automation" --output ./new-project
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Options:**
|
|
125
|
+
|
|
126
|
+
| Flag | Description |
|
|
127
|
+
|------|-------------|
|
|
128
|
+
| `-y, --yes` | skip prompts, use defaults |
|
|
129
|
+
| `--type <type>` | site type: saas, ecommerce, local, blog, marketplace |
|
|
130
|
+
| `--brand <name>` | brand name |
|
|
131
|
+
| `--domain <domain>` | primary domain |
|
|
132
|
+
| `--primary-keyword <keyword>` | primary keyword |
|
|
133
|
+
| `--competitors <list>` | comma-separated competitor list |
|
|
134
|
+
| `--output <dir>` | scaffold into a new (empty) directory instead of cwd |
|
|
135
|
+
|
|
136
|
+
**Behavior:**
|
|
137
|
+
|
|
138
|
+
- Interactive flow by default (5 questions)
|
|
139
|
+
- Non-interactive when `-y` is set **OR** any config flag (`--brand`, `--domain`, `--primary-keyword`, `--competitors`) is provided
|
|
140
|
+
- `--type` alone pre-selects site type in the interactive flow
|
|
141
|
+
- `--output` writes all files to the target directory (must be empty or non-existent)
|
|
95
142
|
|
|
96
143
|
### `seomd validate`
|
|
97
144
|
|
|
@@ -109,7 +156,7 @@ Runs an AI search audit via your connected platform and writes results back into
|
|
|
109
156
|
|
|
110
157
|
- `SEO.md` (`_analysis` blocks)
|
|
111
158
|
- `SEO.REVERSE.md` (generated reverse view)
|
|
112
|
-
- `.
|
|
159
|
+
- `.seo/pages/*.md` (per-page playbooks when available)
|
|
113
160
|
|
|
114
161
|
### `seomd sync`
|
|
115
162
|
|
|
@@ -117,6 +164,25 @@ Pulls cached/latest platform intelligence and writes it back to the same files a
|
|
|
117
164
|
|
|
118
165
|
- `--dry-run` prints a preview and does not modify files
|
|
119
166
|
|
|
167
|
+
## Built-in Templates
|
|
168
|
+
|
|
169
|
+
`seomd-cli` ships with type-specific templates under `src/templates/`. `seomd init --type <type>` uses the matching template automatically.
|
|
170
|
+
|
|
171
|
+
| Type | Template dir | Best for |
|
|
172
|
+
|------|-------------|----------|
|
|
173
|
+
| `saas` | `src/templates/saas/` | Software products, B2B tools, web apps |
|
|
174
|
+
| `blog` | `src/templates/blog/` | Content sites, newsletters, personal brands |
|
|
175
|
+
| `ecommerce` | `src/templates/ecommerce/` | Online stores, DTC brands, product catalogs |
|
|
176
|
+
| `local` | `src/templates/local/` | Service-area businesses, locations pages |
|
|
177
|
+
| `marketplace` | `src/templates/marketplace/` | Two-sided platforms, directories |
|
|
178
|
+
|
|
179
|
+
Each template contains:
|
|
180
|
+
|
|
181
|
+
- `SEO.md` — pre-filled with type-specific intent queries, page structures, and negative keywords
|
|
182
|
+
- `SEO.REVERSE.md` — reverse-engineer output scaffold with placeholders for competitor analysis
|
|
183
|
+
|
|
184
|
+
Want to customize a template? Copy the relevant folder, edit the placeholders (`{{brand}}`, `{{domain}}`, etc.), and use `--type` with your custom scaffold.
|
|
185
|
+
|
|
120
186
|
## Local Development
|
|
121
187
|
|
|
122
188
|
Prefer the local entrypoint while developing:
|
|
@@ -162,9 +228,11 @@ To enable live intelligence writebacks (using automated platforms like [Foxcite]
|
|
|
162
228
|
|
|
163
229
|
1. Obtain a developer API key from your platform provider.
|
|
164
230
|
2. Export the key as an environment variable:
|
|
231
|
+
|
|
165
232
|
```bash
|
|
166
233
|
export SEOMD_API_KEY="your_api_key_here"
|
|
167
234
|
```
|
|
235
|
+
|
|
168
236
|
3. Run `seomd sync` or `seomd analyze`.
|
|
169
237
|
|
|
170
238
|
_Note: Never commit your API keys or `.env` files containing keys to version control._
|
package/bin/seomd.js
CHANGED
|
@@ -25,6 +25,19 @@ program
|
|
|
25
25
|
.description('Scaffold SEO.md for your project')
|
|
26
26
|
.option('-y, --yes', 'skip prompts and use defaults')
|
|
27
27
|
.option('--type <type>', 'site type: saas, ecommerce, local, blog, marketplace')
|
|
28
|
+
.option('--brand <brand>', 'brand name')
|
|
29
|
+
.option('--domain <domain>', 'primary domain')
|
|
30
|
+
.option('--primary-keyword <keyword>', 'primary keyword')
|
|
31
|
+
.option('--competitors <list>', 'comma-separated competitor list')
|
|
32
|
+
.option('--output <dir>', 'scaffold into a new directory instead of cwd')
|
|
33
|
+
.addHelpText('after', `
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
seomd init # interactive 5-question flow
|
|
37
|
+
seomd init -y --type local # skip prompts, use defaults
|
|
38
|
+
seomd init --brand "Acme" --domain acme.com --primary-keyword "local seo"
|
|
39
|
+
seomd init --type saas --brand "MyApp" --domain myapp.com --output ./new-project
|
|
40
|
+
`)
|
|
28
41
|
.action(initCommand);
|
|
29
42
|
|
|
30
43
|
program
|
|
@@ -33,23 +46,47 @@ program
|
|
|
33
46
|
.option('--page <url>', 'analyze a specific page only')
|
|
34
47
|
.option('--intent <category>', 'analyze a specific intent category only')
|
|
35
48
|
.option('--engines <list>', 'comma-separated list of engines to scan (e.g. chatgpt,claude)')
|
|
49
|
+
.addHelpText('after', `
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
seomd analyze # full audit
|
|
53
|
+
seomd analyze --page /pricing # single page audit
|
|
54
|
+
seomd analyze --intent transactional --engines chatgpt,claude
|
|
55
|
+
`)
|
|
36
56
|
.action(analyzeCommand);
|
|
37
57
|
|
|
38
58
|
program
|
|
39
59
|
.command('sync')
|
|
40
60
|
.description('Sync latest platform intelligence to your SEO.md files')
|
|
41
61
|
.option('--dry-run', 'preview changes without writing')
|
|
62
|
+
.addHelpText('after', `
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
seomd sync # sync latest data
|
|
66
|
+
seomd sync --dry-run # preview changes only
|
|
67
|
+
`)
|
|
42
68
|
.action(syncCommand);
|
|
43
69
|
|
|
44
70
|
program
|
|
45
71
|
.command('status')
|
|
46
72
|
.description('Show current citation rates and gap scores')
|
|
47
73
|
.option('--json', 'output as JSON')
|
|
74
|
+
.addHelpText('after', `
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
seomd status # human-readable summary
|
|
78
|
+
seomd status --json # machine-readable JSON
|
|
79
|
+
`)
|
|
48
80
|
.action(statusCommand);
|
|
49
81
|
|
|
50
82
|
program
|
|
51
83
|
.command('validate')
|
|
52
84
|
.description('Validate your SEO.md against the spec')
|
|
85
|
+
.addHelpText('after', `
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
seomd validate
|
|
89
|
+
`)
|
|
53
90
|
.action(validateCommand);
|
|
54
91
|
|
|
55
92
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seomd-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "The official CLI for the SEO.md open standard — AEO infrastructure for technical founders",
|
|
5
5
|
"homepage": "https://seomd.dev",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,19 @@
|
|
|
25
25
|
"release:notes": "node scripts/release-notes.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"axios": "^1.6.0",
|
|
28
29
|
"chalk": "^5.3.0",
|
|
29
30
|
"commander": "^11.0.0",
|
|
31
|
+
"dotenv": "^16.3.1",
|
|
30
32
|
"enquirer": "^2.4.1",
|
|
31
|
-
"ora": "^7.0.1",
|
|
32
|
-
"yaml": "^2.3.4",
|
|
33
33
|
"fs-extra": "^11.1.1",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"ora": "^7.0.1",
|
|
35
|
+
"yaml": "^2.3.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"
|
|
39
|
-
"eslint": "^
|
|
38
|
+
"eslint": "^8.50.0",
|
|
39
|
+
"eslint-plugin-n": "^18.1.0",
|
|
40
|
+
"jest": "^29.7.0"
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=18.0.0"
|
package/src/commands/analyze.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
-
import path from 'path';
|
|
5
4
|
import dotenv from 'dotenv';
|
|
6
5
|
import { parseSeoMd } from '../utils/parser.js';
|
|
7
6
|
import { client } from '../utils/api-client.js';
|
|
@@ -12,16 +11,16 @@ dotenv.config();
|
|
|
12
11
|
function matchRoute(pattern, url) {
|
|
13
12
|
const cleanPattern = pattern.replace(/\/$/, '');
|
|
14
13
|
const cleanUrl = url.replace(/\/$/, '');
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
if (cleanPattern.toLowerCase() === cleanUrl.toLowerCase()) {
|
|
17
16
|
return true;
|
|
18
17
|
}
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
// Replace "/[param]" with optional group "(?:/([^/]+))?"
|
|
21
20
|
const regexPattern = cleanPattern
|
|
22
21
|
.replace(/\/\[[^\]]+\]/g, '(?:\\/([^/]+))?')
|
|
23
22
|
.replace(/\//g, '\\/');
|
|
24
|
-
|
|
23
|
+
|
|
25
24
|
const regex = new RegExp('^' + regexPattern + '\\/?$', 'i');
|
|
26
25
|
return regex.test(url);
|
|
27
26
|
}
|
|
@@ -148,7 +147,7 @@ export async function analyzeCommand(options) {
|
|
|
148
147
|
const brandName = data.identity?.brand || 'My Brand';
|
|
149
148
|
await writeReverseMd(cwd, results, domain, brandName);
|
|
150
149
|
|
|
151
|
-
// Writeback to .
|
|
150
|
+
// Writeback to .seo/pages/*.md
|
|
152
151
|
await writePageAnalysis(cwd, results);
|
|
153
152
|
|
|
154
153
|
spinner.succeed(chalk.green('Analysis completed successfully!'));
|
|
@@ -169,7 +168,7 @@ export async function analyzeCommand(options) {
|
|
|
169
168
|
console.log('');
|
|
170
169
|
console.log(chalk.green('✔ SEO.md updated.'));
|
|
171
170
|
console.log(chalk.green('✔ SEO.REVERSE.md updated.'));
|
|
172
|
-
console.log(chalk.green('✔ .
|
|
171
|
+
console.log(chalk.green('✔ .seo/pages/ playbooks generated.'));
|
|
173
172
|
console.log('');
|
|
174
173
|
|
|
175
174
|
} catch (err) {
|
package/src/commands/init.js
CHANGED
|
@@ -16,8 +16,19 @@ export async function initCommand(options) {
|
|
|
16
16
|
console.log(chalk.dim('The open standard for AI-era SEO configuration.'));
|
|
17
17
|
console.log('');
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
// Determine target directory
|
|
20
|
+
let workingDir = process.cwd();
|
|
21
|
+
if (options.output) {
|
|
22
|
+
const resolved = path.resolve(options.output);
|
|
23
|
+
if (await fs.pathExists(resolved) && (await fs.readdir(resolved)).length > 0) {
|
|
24
|
+
console.log(chalk.red(`Output directory must be empty or non-existent: ${resolved}`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
await fs.ensureDir(resolved);
|
|
28
|
+
workingDir = resolved;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const seomdPath = path.join(workingDir, 'SEO.md');
|
|
21
32
|
if (await fs.pathExists(seomdPath)) {
|
|
22
33
|
console.log(chalk.yellow('⚠ SEO.md already exists in this directory.'));
|
|
23
34
|
const { overwrite } = await prompt({
|
|
@@ -34,14 +45,26 @@ export async function initCommand(options) {
|
|
|
34
45
|
|
|
35
46
|
let answers;
|
|
36
47
|
|
|
37
|
-
if
|
|
38
|
-
|
|
48
|
+
// Mode 5: non-interactive if -y OR any config field provided
|
|
49
|
+
const hasConfigFlags =
|
|
50
|
+
options.brand !== undefined ||
|
|
51
|
+
options.domain !== undefined ||
|
|
52
|
+
options.primaryKeyword !== undefined ||
|
|
53
|
+
options.competitors !== undefined;
|
|
54
|
+
|
|
55
|
+
if (options.yes || hasConfigFlags) {
|
|
39
56
|
answers = {
|
|
40
57
|
site_type: options.type || 'saas',
|
|
41
|
-
domain: 'example.com',
|
|
42
|
-
brand: 'My Brand',
|
|
43
|
-
primary_keyword: '',
|
|
44
|
-
competitors:
|
|
58
|
+
domain: options.domain || 'example.com',
|
|
59
|
+
brand: options.brand || 'My Brand',
|
|
60
|
+
primary_keyword: options.primaryKeyword || '',
|
|
61
|
+
competitors: options.competitors
|
|
62
|
+
? options.competitors
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((c) => c.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.slice(0, 3)
|
|
67
|
+
: [],
|
|
45
68
|
};
|
|
46
69
|
} else {
|
|
47
70
|
// The 5-question init flow
|
|
@@ -109,21 +132,20 @@ export async function initCommand(options) {
|
|
|
109
132
|
try {
|
|
110
133
|
// 1. Generate SEO.md
|
|
111
134
|
const seomdContent = generateSeoMd(answers);
|
|
112
|
-
await fs.writeFile(
|
|
135
|
+
await fs.writeFile(path.join(workingDir, 'SEO.md'), seomdContent, 'utf8');
|
|
113
136
|
spinner.succeed(chalk.green('SEO.md created'));
|
|
114
137
|
|
|
115
138
|
// 2. Generate SEO.REVERSE.md
|
|
116
|
-
const reversePath = path.join(process.cwd(), 'SEO.REVERSE.md');
|
|
117
139
|
const reverseContent = generateReverseMd(answers);
|
|
118
|
-
await fs.writeFile(
|
|
140
|
+
await fs.writeFile(path.join(workingDir, 'SEO.REVERSE.md'), reverseContent, 'utf8');
|
|
119
141
|
spinner.succeed(chalk.green('SEO.REVERSE.md initialized'));
|
|
120
142
|
|
|
121
|
-
// 3. Create .
|
|
122
|
-
await createSeomdDir(
|
|
123
|
-
spinner.succeed(chalk.green('.
|
|
143
|
+
// 3. Create .seo/ directory structure
|
|
144
|
+
await createSeomdDir(workingDir, answers);
|
|
145
|
+
spinner.succeed(chalk.green('.seo/ directory created'));
|
|
124
146
|
|
|
125
|
-
// 4. Add .
|
|
126
|
-
await updateGitignore(
|
|
147
|
+
// 4. Add .seo/ to .gitignore if it exists
|
|
148
|
+
await updateGitignore(workingDir);
|
|
127
149
|
|
|
128
150
|
console.log('');
|
|
129
151
|
console.log(chalk.bold.green('✓ SEO.md initialized successfully'));
|
|
@@ -131,7 +153,7 @@ export async function initCommand(options) {
|
|
|
131
153
|
console.log(chalk.dim('Files created:'));
|
|
132
154
|
console.log(' ' + chalk.cyan('SEO.md') + chalk.dim(' — your living SEO config'));
|
|
133
155
|
console.log(' ' + chalk.cyan('SEO.REVERSE.md') + chalk.dim(' — reverse engineer output (platform generated)'));
|
|
134
|
-
console.log(' ' + chalk.cyan('.
|
|
156
|
+
console.log(' ' + chalk.cyan('.seo/') + chalk.dim(' — intelligence directory'));
|
|
135
157
|
console.log('');
|
|
136
158
|
console.log(chalk.dim('Next steps:'));
|
|
137
159
|
console.log(' ' + chalk.white('npx seomd analyze') + chalk.dim(' — run your first citation analysis'));
|
|
@@ -149,11 +171,11 @@ export async function initCommand(options) {
|
|
|
149
171
|
|
|
150
172
|
async function updateGitignore(cwd) {
|
|
151
173
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
152
|
-
const entry = '\n# seomd intelligence directory\n.
|
|
174
|
+
const entry = '\n# seomd intelligence directory\n.seo/reports/\n';
|
|
153
175
|
|
|
154
176
|
if (await fs.pathExists(gitignorePath)) {
|
|
155
177
|
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
156
|
-
if (!content.includes('.
|
|
178
|
+
if (!content.includes('.seo')) {
|
|
157
179
|
await fs.appendFile(gitignorePath, entry);
|
|
158
180
|
}
|
|
159
181
|
}
|
package/src/commands/sync.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
-
import path from 'path';
|
|
5
4
|
import dotenv from 'dotenv';
|
|
6
5
|
import { parseSeoMd } from '../utils/parser.js';
|
|
7
6
|
import { client } from '../utils/api-client.js';
|
|
@@ -91,7 +90,7 @@ export async function syncCommand(options) {
|
|
|
91
90
|
const brandName = data.identity?.brand || 'My Brand';
|
|
92
91
|
await writeReverseMd(cwd, results, domain, brandName);
|
|
93
92
|
|
|
94
|
-
// Writeback to .
|
|
93
|
+
// Writeback to .seo/pages/*.md
|
|
95
94
|
await writePageAnalysis(cwd, results);
|
|
96
95
|
|
|
97
96
|
spinner.succeed(chalk.green('Sync completed successfully!'));
|
|
@@ -112,7 +111,7 @@ export async function syncCommand(options) {
|
|
|
112
111
|
console.log('');
|
|
113
112
|
console.log(chalk.green('✔ SEO.md updated.'));
|
|
114
113
|
console.log(chalk.green('✔ SEO.REVERSE.md updated.'));
|
|
115
|
-
console.log(chalk.green('✔ .
|
|
114
|
+
console.log(chalk.green('✔ .seo/pages/ playbooks synchronized.'));
|
|
116
115
|
console.log('');
|
|
117
116
|
|
|
118
117
|
} catch (err) {
|
|
@@ -5,14 +5,14 @@ import { REQUIRED_PAGES } from '../utils/constants.js';
|
|
|
5
5
|
export async function createSeomdDir(cwd, answers) {
|
|
6
6
|
const { site_type, brand, domain } = answers;
|
|
7
7
|
const pages = REQUIRED_PAGES[site_type] || REQUIRED_PAGES.saas;
|
|
8
|
-
const seomdDir = path.join(cwd, '.
|
|
8
|
+
const seomdDir = path.join(cwd, '.seo');
|
|
9
9
|
|
|
10
10
|
// Create directory structure
|
|
11
11
|
await fs.ensureDir(path.join(seomdDir, 'pages'));
|
|
12
12
|
await fs.ensureDir(path.join(seomdDir, 'reports'));
|
|
13
13
|
await fs.ensureDir(path.join(seomdDir, 'competitors'));
|
|
14
14
|
|
|
15
|
-
// Create README inside .
|
|
15
|
+
// Create README inside .seo/
|
|
16
16
|
await fs.writeFile(
|
|
17
17
|
path.join(seomdDir, 'README.md'),
|
|
18
18
|
generateSeomdReadme(brand, domain),
|
|
@@ -38,40 +38,91 @@ export async function createSeomdDir(cwd, answers) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function generateSeomdReadme(brand, domain) {
|
|
41
|
-
return `# .
|
|
41
|
+
return `# .seo/
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
AI search optimization workspace for **${brand}** (${domain})
|
|
44
44
|
Generated by SEO.md CLI — https://seomd.dev
|
|
45
45
|
|
|
46
|
+
## What is this?
|
|
47
|
+
|
|
48
|
+
\`.seo/\` stores platform-generated intelligence from citation audits, competitor analysis, and reverse-engineered playbooks. It lives alongside your source code so SEO strategy is version-controlled and reviewable in PRs.
|
|
49
|
+
|
|
46
50
|
## Directory Structure
|
|
47
51
|
|
|
48
52
|
\`\`\`
|
|
49
|
-
.
|
|
50
|
-
├── pages/ # per-page reverse
|
|
53
|
+
.seo/
|
|
54
|
+
├── pages/ # per-page reverse-engineer analysis
|
|
55
|
+
│ ├── homepage.md
|
|
56
|
+
│ ├── services.md
|
|
57
|
+
│ └── ...
|
|
51
58
|
├── reports/ # dated snapshot reports (gitignored by default)
|
|
59
|
+
│ └── 2026-06-10.md
|
|
52
60
|
└── competitors/ # competitor citation profiles
|
|
61
|
+
└── comp-1.md
|
|
53
62
|
\`\`\`
|
|
54
63
|
|
|
55
|
-
##
|
|
64
|
+
## Git behavior
|
|
65
|
+
|
|
66
|
+
| Path | Git | Why |
|
|
67
|
+
|------|-----|-----|
|
|
68
|
+
| \`.seo/pages/\` | tracked | actionable playbooks belong in code review |
|
|
69
|
+
| \`.seo/competitors/\` | tracked | competitor intel is project state |
|
|
70
|
+
| \`.seo/reports/\` | ignored | dated snapshots are noise in git history |
|
|
71
|
+
|
|
72
|
+
\`.seo/reports/\` is added to \`.gitignore\` automatically by \`seomd init\`.
|
|
73
|
+
|
|
74
|
+
## When to run what
|
|
56
75
|
|
|
57
76
|
\`\`\`bash
|
|
58
|
-
#
|
|
77
|
+
# First full audit — scans all pages + intents
|
|
59
78
|
npx seomd analyze
|
|
60
79
|
|
|
61
|
-
#
|
|
80
|
+
# Refresh from cached platform data (no new scan)
|
|
62
81
|
npx seomd sync
|
|
63
82
|
|
|
64
|
-
#
|
|
83
|
+
# Check current gap scores without modifying files
|
|
65
84
|
npx seomd status
|
|
85
|
+
npx seomd status --json
|
|
66
86
|
\`\`\`
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
| Command | Use when |
|
|
89
|
+
|---------|----------|
|
|
90
|
+
| \`analyze\` | You want a fresh audit, new competitor data, or updated playbooks |
|
|
91
|
+
| \`sync\` | You just want the latest cached data from your platform |
|
|
92
|
+
| \`status\` | You want a quick health check without writebacks |
|
|
93
|
+
|
|
94
|
+
## Reading a page playbook
|
|
95
|
+
|
|
96
|
+
Each \`pages/<id>.md\` contains:
|
|
97
|
+
|
|
98
|
+
- **why_page_won** — what top-cited competitors do right
|
|
99
|
+
- **citation_hooks** — patterns to replicate
|
|
100
|
+
- **gaps_for_brand** — where your page falls short
|
|
101
|
+
- **remediation_playbook** — step-by-step fixes with effort/impact
|
|
102
|
+
- **fastest_win** — highest ROI action to take now
|
|
103
|
+
- **suggested_content_outline** — ready-to-use outline for your writer or AI
|
|
104
|
+
|
|
105
|
+
## File ownership
|
|
106
|
+
|
|
107
|
+
| File/dir | Owner | Editable? |
|
|
108
|
+
|-----------|-------|----------|
|
|
109
|
+
| \`SEO.md\` | you | yes |
|
|
110
|
+
| \`SEO.REVERSE.md\` | platform | no |
|
|
111
|
+
| \`.seo/pages/*.md\` | platform | no |
|
|
112
|
+
| \`.seo/competitors/*.md\` | platform | no |
|
|
113
|
+
| \`.seo/reports/*.md\` | platform | no |
|
|
114
|
+
|
|
115
|
+
Platform-generated files are overwritten on each \`analyze\` or \`sync\`.
|
|
116
|
+
|
|
117
|
+
## Platform connection
|
|
118
|
+
|
|
119
|
+
To enable live writebacks:
|
|
69
120
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
121
|
+
1. Connect at https://seomd.dev/connect
|
|
122
|
+
2. Add \`SEOMD_API_KEY\` to your \`.env\`
|
|
123
|
+
3. Run \`npx seomd analyze\`
|
|
73
124
|
|
|
74
|
-
|
|
125
|
+
_Note: Never commit API keys or \`.env\` files to version control._
|
|
75
126
|
`;
|
|
76
127
|
}
|
|
77
128
|
|
package/src/utils/writeback.js
CHANGED
|
@@ -110,13 +110,13 @@ export async function writeReverseMd(cwd, response, defaultDomain = 'example.com
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Writes individual page playbooks into .
|
|
113
|
+
* Writes individual page playbooks into .seo/pages/{id}.md files.
|
|
114
114
|
*
|
|
115
115
|
* @param {string} cwd - Current working directory
|
|
116
116
|
* @param {any} response - The API response from analyze/sync
|
|
117
117
|
*/
|
|
118
118
|
export async function writePageAnalysis(cwd, response) {
|
|
119
|
-
const seomdDir = path.join(cwd, '.
|
|
119
|
+
const seomdDir = path.join(cwd, '.seo');
|
|
120
120
|
const pagesDir = path.join(seomdDir, 'pages');
|
|
121
121
|
|
|
122
122
|
await fs.ensureDir(pagesDir);
|