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,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import { parseSeoMd } from '../utils/parser.js';
|
|
7
|
+
import { client } from '../utils/api-client.js';
|
|
8
|
+
import { writeAnalysisToSeoMd, writeReverseMd, writePageAnalysis } from '../utils/writeback.js';
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
export async function syncCommand(options) {
|
|
13
|
+
const apiKey = process.env.SEOMD_API_KEY;
|
|
14
|
+
const paymentToken = process.env.SEOMD_PAYMENT_TOKEN;
|
|
15
|
+
|
|
16
|
+
if (!apiKey && !paymentToken) {
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(chalk.yellow('ā No API key or payment token found.'));
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log('Add your API key to .env:');
|
|
21
|
+
console.log(chalk.cyan(' SEOMD_API_KEY=your_key_here'));
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log('Get your API key at ' + chalk.cyan('https://seomd.dev/connect'));
|
|
24
|
+
console.log('');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
let doc, data;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const parsed = await parseSeoMd(cwd);
|
|
33
|
+
doc = parsed.doc;
|
|
34
|
+
data = parsed.data;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.log(chalk.red(`\nā Error: ${err.message}`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const domain = data.site?.domain;
|
|
41
|
+
if (!domain) {
|
|
42
|
+
console.log(chalk.red('\nā Error: "site.domain" is required in SEO.md.'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract pages
|
|
47
|
+
let pagesList = [];
|
|
48
|
+
if (data.pages) {
|
|
49
|
+
if (Array.isArray(data.pages.required)) {
|
|
50
|
+
pagesList = pagesList.concat(data.pages.required);
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(data.pages.optional)) {
|
|
53
|
+
pagesList = pagesList.concat(data.pages.optional);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.bold.cyan(`\nš foxcite: Syncing AI Search Audit for ${chalk.white(domain)}`));
|
|
58
|
+
const spinner = ora('Fetching cached analysis from platform...').start();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const pagesParam = JSON.stringify(pagesList.map(p => ({
|
|
62
|
+
id: p.id,
|
|
63
|
+
url: p.url,
|
|
64
|
+
primary_keyword: p.primary_keyword
|
|
65
|
+
})));
|
|
66
|
+
|
|
67
|
+
const response = await client.get('/cli/sync', {
|
|
68
|
+
params: { domain, pages: pagesParam }
|
|
69
|
+
});
|
|
70
|
+
const results = response.data;
|
|
71
|
+
|
|
72
|
+
if (options.dryRun) {
|
|
73
|
+
spinner.succeed(chalk.green('Sync check completed (Dry Run)!'));
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.bold('--- Dry-Run Updates Preview ---'));
|
|
76
|
+
const aeo = results.aeo_analysis;
|
|
77
|
+
console.log(`Overall Citation Rate : ${chalk.bold.green((aeo.overall_citation_rate * 100).toFixed(0) + '%')}`);
|
|
78
|
+
console.log(`Overall Gap Score : ${chalk.bold.red(aeo.overall_gap_score)}`);
|
|
79
|
+
console.log(`Last Analyzed : ${chalk.dim(aeo.last_analyzed)}`);
|
|
80
|
+
console.log(chalk.yellow('\nā Dry-run enabled: No files were modified.'));
|
|
81
|
+
console.log('');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
spinner.text = 'Updating repository files...';
|
|
86
|
+
|
|
87
|
+
// Writeback to SEO.md
|
|
88
|
+
await writeAnalysisToSeoMd(doc, results, cwd);
|
|
89
|
+
|
|
90
|
+
// Writeback to SEO.REVERSE.md
|
|
91
|
+
await writeReverseMd(cwd, results);
|
|
92
|
+
|
|
93
|
+
// Writeback to .seomd/pages/*.md
|
|
94
|
+
await writePageAnalysis(cwd, results);
|
|
95
|
+
|
|
96
|
+
spinner.succeed(chalk.green('Sync completed successfully!'));
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
// Display results summary
|
|
100
|
+
const aeo = results.aeo_analysis;
|
|
101
|
+
console.log(chalk.bold('--- Sync Results Summary ---'));
|
|
102
|
+
console.log(`Overall Citation Rate : ${chalk.bold.green((aeo.overall_citation_rate * 100).toFixed(0) + '%')}`);
|
|
103
|
+
console.log(`Overall Gap Score : ${chalk.bold.red(aeo.overall_gap_score)}`);
|
|
104
|
+
|
|
105
|
+
if (results.credits_remaining !== null) {
|
|
106
|
+
console.log(`Credits Remaining : ${chalk.cyan(results.credits_remaining)}`);
|
|
107
|
+
}
|
|
108
|
+
console.log(`Last Analyzed : ${chalk.dim(aeo.last_analyzed)}`);
|
|
109
|
+
console.log(`Next Analysis Target : ${chalk.dim(aeo.next_analysis)}`);
|
|
110
|
+
console.log('----------------------------');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.green('ā SEO.md updated.'));
|
|
113
|
+
console.log(chalk.green('ā SEO.REVERSE.md updated.'));
|
|
114
|
+
console.log(chalk.green('ā .seomd/pages/ playbooks synchronized.'));
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
} catch (err) {
|
|
118
|
+
spinner.fail(chalk.red('Sync failed'));
|
|
119
|
+
console.error(chalk.bold.red(`\nError: ${err.message}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { parseSeoMd } from '../utils/parser.js';
|
|
3
|
+
import { validateSeoMd } from '../validators/seomd.js';
|
|
4
|
+
|
|
5
|
+
export async function validateCommand() {
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log(chalk.bold('Validating SEO.md...') + '\n');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const { data } = await parseSeoMd(process.cwd());
|
|
11
|
+
const { errors, warnings } = validateSeoMd(data);
|
|
12
|
+
|
|
13
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
14
|
+
console.log(chalk.green(' ā ') + chalk.bold('SEO.md is fully compliant with spec v1.0!'));
|
|
15
|
+
console.log('');
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Print errors
|
|
20
|
+
if (errors.length > 0) {
|
|
21
|
+
console.log(chalk.red.bold(` Errors (${errors.length}):`));
|
|
22
|
+
errors.forEach(err => {
|
|
23
|
+
const pathStr = err.path ? chalk.dim(`[${err.path}]`) : '';
|
|
24
|
+
console.log(` ${chalk.red('ā')} ${err.message} ${pathStr}`);
|
|
25
|
+
});
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Print warnings
|
|
30
|
+
if (warnings.length > 0) {
|
|
31
|
+
console.log(chalk.yellow.bold(` Warnings (${warnings.length}):`));
|
|
32
|
+
warnings.forEach(warn => {
|
|
33
|
+
const pathStr = warn.path ? chalk.dim(`[${warn.path}]`) : '';
|
|
34
|
+
console.log(` ${chalk.yellow('ā ')} ${warn.message} ${pathStr}`);
|
|
35
|
+
});
|
|
36
|
+
console.log('');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Final status report
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
console.log(chalk.red.bold(`ā Validation failed: ${errors.length} error(s) and ${warnings.length} warning(s) found.`));
|
|
42
|
+
console.log(chalk.dim('Please fix the errors to make your SEO.md valid.'));
|
|
43
|
+
console.log('');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.yellow.bold(`ā Validation passed with ${warnings.length} warning(s).`));
|
|
47
|
+
console.log(chalk.dim('Warnings are optional recommendations and do not block compliance.'));
|
|
48
|
+
console.log('');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.log(chalk.red('ā ') + chalk.bold('Validation process failed:'));
|
|
54
|
+
console.log(` ${chalk.dim(err.message)}`);
|
|
55
|
+
console.log('');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { REQUIRED_PAGES } from '../utils/constants.js';
|
|
4
|
+
|
|
5
|
+
export async function createSeomdDir(cwd, answers) {
|
|
6
|
+
const { site_type, brand, domain } = answers;
|
|
7
|
+
const pages = REQUIRED_PAGES[site_type] || REQUIRED_PAGES.saas;
|
|
8
|
+
const seomdDir = path.join(cwd, '.seomd');
|
|
9
|
+
|
|
10
|
+
// Create directory structure
|
|
11
|
+
await fs.ensureDir(path.join(seomdDir, 'pages'));
|
|
12
|
+
await fs.ensureDir(path.join(seomdDir, 'reports'));
|
|
13
|
+
await fs.ensureDir(path.join(seomdDir, 'competitors'));
|
|
14
|
+
|
|
15
|
+
// Create README inside .seomd/
|
|
16
|
+
await fs.writeFile(
|
|
17
|
+
path.join(seomdDir, 'README.md'),
|
|
18
|
+
generateSeomdReadme(brand, domain),
|
|
19
|
+
'utf8'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Create placeholder page files
|
|
23
|
+
for (const page of pages) {
|
|
24
|
+
await fs.writeFile(
|
|
25
|
+
path.join(seomdDir, 'pages', `${page.id}.md`),
|
|
26
|
+
generatePagePlaceholder(page, brand),
|
|
27
|
+
'utf8'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create initial empty report
|
|
32
|
+
const date = new Date().toISOString().split('T')[0];
|
|
33
|
+
await fs.writeFile(
|
|
34
|
+
path.join(seomdDir, 'reports', `${date}.md`),
|
|
35
|
+
generateInitialReport(brand, domain, date),
|
|
36
|
+
'utf8'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateSeomdReadme(brand, domain) {
|
|
41
|
+
return `# .seomd/
|
|
42
|
+
|
|
43
|
+
Intelligence directory for ${brand} (${domain})
|
|
44
|
+
Generated by SEO.md CLI ā https://seomd.dev
|
|
45
|
+
|
|
46
|
+
## Directory Structure
|
|
47
|
+
|
|
48
|
+
\`\`\`
|
|
49
|
+
.seomd/
|
|
50
|
+
āāā pages/ # per-page reverse engineer analysis
|
|
51
|
+
āāā reports/ # dated snapshot reports (gitignored by default)
|
|
52
|
+
āāā competitors/ # competitor citation profiles
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
# Run citation analysis and update all files
|
|
59
|
+
npx seomd analyze
|
|
60
|
+
|
|
61
|
+
# Sync latest platform intelligence
|
|
62
|
+
npx seomd sync
|
|
63
|
+
|
|
64
|
+
# View current status
|
|
65
|
+
npx seomd status
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
## File Ownership
|
|
69
|
+
|
|
70
|
+
- \`pages/\` ā platform generated, do not edit manually
|
|
71
|
+
- \`reports/\` ā platform generated, gitignored by default
|
|
72
|
+
- \`competitors/\` ā platform generated, do not edit manually
|
|
73
|
+
|
|
74
|
+
Connect your platform at https://seomd.dev/connect
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function generatePagePlaceholder(page, brand) {
|
|
79
|
+
const date = new Date().toISOString().split('T')[0];
|
|
80
|
+
return `# ${page.id}
|
|
81
|
+
|
|
82
|
+
## Page: ${page.url}
|
|
83
|
+
|
|
84
|
+
### Priority: ${page.priority}
|
|
85
|
+
|
|
86
|
+
### Last analyzed: null
|
|
87
|
+
|
|
88
|
+
### Run \`npx seomd analyze --page ${page.url}\` to populate
|
|
89
|
+
|
|
90
|
+
brand: "${brand}"
|
|
91
|
+
page_id: ${page.id}
|
|
92
|
+
url: ${page.url}
|
|
93
|
+
status: planned
|
|
94
|
+
citation_rate: null
|
|
95
|
+
gap_score: null
|
|
96
|
+
top_cited_competitor: null
|
|
97
|
+
last_analyzed: null
|
|
98
|
+
|
|
99
|
+
why_page_won: []
|
|
100
|
+
citation_hooks: []
|
|
101
|
+
gaps_for_brand: []
|
|
102
|
+
remediation_playbook: []
|
|
103
|
+
|
|
104
|
+
fastest_win:
|
|
105
|
+
action: null
|
|
106
|
+
estimated_gap_score_improvement: null
|
|
107
|
+
effort: null
|
|
108
|
+
|
|
109
|
+
suggested_content_outline: []
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function generateInitialReport(brand, domain, date) {
|
|
114
|
+
return `# SEO.md Report ā ${date}
|
|
115
|
+
|
|
116
|
+
## ${brand} (${domain})
|
|
117
|
+
|
|
118
|
+
### Initial report ā run \`npx seomd analyze\` to populate
|
|
119
|
+
|
|
120
|
+
date: ${date}
|
|
121
|
+
brand: "${brand}"
|
|
122
|
+
domain: ${domain}
|
|
123
|
+
overall_citation_rate: null
|
|
124
|
+
overall_gap_score: null
|
|
125
|
+
|
|
126
|
+
intent_summary:
|
|
127
|
+
informational: null
|
|
128
|
+
comparison: null
|
|
129
|
+
transactional: null
|
|
130
|
+
reputational: null
|
|
131
|
+
category: null
|
|
132
|
+
|
|
133
|
+
top_opportunities: []
|
|
134
|
+
completed_wins: []
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates the content of the seo.reverse.md file by reading the corresponding site type template
|
|
9
|
+
* and substituting placeholders.
|
|
10
|
+
*
|
|
11
|
+
* @param {any} answers - Scaffolding inputs (brand, domain, primary_keyword, site_type, competitors)
|
|
12
|
+
* @returns {string} The filled template content
|
|
13
|
+
*/
|
|
14
|
+
export function generateReverseMd(answers) {
|
|
15
|
+
const { site_type, domain, brand, competitors } = answers;
|
|
16
|
+
const competitorList = Array.isArray(competitors) ? competitors : [];
|
|
17
|
+
|
|
18
|
+
// Resolve template file path
|
|
19
|
+
const templatePath = path.join(__dirname, '../templates', site_type, 'SEO.REVERSE.md');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(templatePath)) {
|
|
22
|
+
throw new Error(`Template not found for site type: ${site_type}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let content = fs.readFileSync(templatePath, 'utf8');
|
|
26
|
+
|
|
27
|
+
const date = new Date().toISOString().split('T')[0];
|
|
28
|
+
const brandLower = brand.toLowerCase();
|
|
29
|
+
const brandSnake = brandLower.replace(/\s+/g, '_');
|
|
30
|
+
const primaryCompetitor = competitorList[0] || '[competitor]';
|
|
31
|
+
|
|
32
|
+
// Formatting YAML blocks for competitors
|
|
33
|
+
let reverseCompetitors = '';
|
|
34
|
+
if (competitorList.length > 0) {
|
|
35
|
+
reverseCompetitors = competitorList.map(c => ` - domain: ${c}
|
|
36
|
+
overall_citation_rate: null
|
|
37
|
+
strongest_intent_category: null
|
|
38
|
+
weakest_intent_category: null
|
|
39
|
+
top_cited_pages: []
|
|
40
|
+
citation_patterns: []
|
|
41
|
+
last_analyzed: null`).join('\n\n');
|
|
42
|
+
} else {
|
|
43
|
+
reverseCompetitors = ` - domain: [competitor]
|
|
44
|
+
overall_citation_rate: null
|
|
45
|
+
strongest_intent_category: null
|
|
46
|
+
weakest_intent_category: null
|
|
47
|
+
top_cited_pages: []
|
|
48
|
+
citation_patterns: []
|
|
49
|
+
last_analyzed: null`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Perform simple string replacements
|
|
53
|
+
content = content
|
|
54
|
+
.replaceAll('{{brand}}', brand)
|
|
55
|
+
.replaceAll('{{brand_lower_snake}}', brandSnake)
|
|
56
|
+
.replaceAll('{{domain}}', domain)
|
|
57
|
+
.replaceAll('{{primary_competitor}}', primaryCompetitor)
|
|
58
|
+
.replaceAll('{{date}}', date)
|
|
59
|
+
.replaceAll('{{reverse_competitors}}', reverseCompetitors);
|
|
60
|
+
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function getBrandName(domain) {
|
|
8
|
+
return domain
|
|
9
|
+
.replace(/^(https?:\/\/)?(www\.)?/, '')
|
|
10
|
+
.replace(/\.[a-z]{2,6}(\.[a-z]{2,6})?$/i, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates the content of the seo.md file by reading the corresponding site type template
|
|
15
|
+
* and substituting placeholders.
|
|
16
|
+
*
|
|
17
|
+
* @param {any} answers - Scaffolding inputs (brand, domain, primary_keyword, site_type, competitors)
|
|
18
|
+
* @returns {string} The filled template content
|
|
19
|
+
*/
|
|
20
|
+
export function generateSeoMd(answers) {
|
|
21
|
+
const { site_type, domain, brand, primary_keyword, competitors } = answers;
|
|
22
|
+
const competitorList = Array.isArray(competitors) ? competitors : [];
|
|
23
|
+
|
|
24
|
+
// Resolve template file path
|
|
25
|
+
const templatePath = path.join(__dirname, '../templates', site_type, 'SEO.md');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(templatePath)) {
|
|
28
|
+
throw new Error(`Template not found for site type: ${site_type}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let content = fs.readFileSync(templatePath, 'utf8');
|
|
32
|
+
|
|
33
|
+
const date = new Date().toISOString().split('T')[0];
|
|
34
|
+
const brandLower = brand.toLowerCase();
|
|
35
|
+
const brandSnake = brandLower.replace(/\s+/g, '_');
|
|
36
|
+
|
|
37
|
+
// Formatting YAML blocks for competitors
|
|
38
|
+
const competitorTerms = competitorList.map(c => ` - "${getBrandName(c)} alternative"`).join('\n') || ' []';
|
|
39
|
+
const comparisonQueries = competitorList.map(c => ` - "${getBrandName(c)} vs ${brandLower}"`).join('\n') || ` - "${brandLower} vs [competitor]"`;
|
|
40
|
+
const toMonitor = competitorList.map(c => ` - ${c}`).join('\n') || ' []';
|
|
41
|
+
|
|
42
|
+
// Perform simple string replacements
|
|
43
|
+
content = content
|
|
44
|
+
.replaceAll('{{brand}}', brand)
|
|
45
|
+
.replaceAll('{{brand_lower}}', brandLower)
|
|
46
|
+
.replaceAll('{{brand_lower_snake}}', brandSnake)
|
|
47
|
+
.replaceAll('{{domain}}', domain)
|
|
48
|
+
.replaceAll('{{primary_keyword}}', primary_keyword)
|
|
49
|
+
.replaceAll('{{date}}', date)
|
|
50
|
+
.replaceAll('{{competitor_terms}}', competitorTerms)
|
|
51
|
+
.replaceAll('{{competitors_comparison_queries}}', comparisonQueries)
|
|
52
|
+
.replaceAll('{{competitors_to_monitor}}', toMonitor);
|
|
53
|
+
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SEO.REVERSE.md
|
|
2
|
+
|
|
3
|
+
## {{brand}}
|
|
4
|
+
|
|
5
|
+
### spec v1.0 | https://seomd.dev
|
|
6
|
+
|
|
7
|
+
#### generated: {{date}}
|
|
8
|
+
|
|
9
|
+
## This file is generated by the {{brand}} platform.
|
|
10
|
+
### Do not edit manually ā run `npx seomd analyze` to update.
|
|
11
|
+
### Run `npx seomd sync` to pull latest intelligence.
|
|
12
|
+
|
|
13
|
+
## Each page section contains a full reverse engineer analysis:
|
|
14
|
+
|
|
15
|
+
### why_page_won ā what the top cited competitor does right
|
|
16
|
+
### citation_hooks ā specific patterns to replicate
|
|
17
|
+
### gaps_for_brand ā where your page falls short
|
|
18
|
+
### remediation_playbook ā step by step fixes with effort/impact
|
|
19
|
+
### fastest_win ā highest ROI action to take now
|
|
20
|
+
### suggested_content_outline ā ready to use outline for your writer/AI
|
|
21
|
+
|
|
22
|
+
domain: {{domain}}
|
|
23
|
+
brand: "{{brand}}"
|
|
24
|
+
primary_competitor: {{primary_competitor}}
|
|
25
|
+
last_analyzed: null
|
|
26
|
+
next_analysis: null
|
|
27
|
+
source: null
|
|
28
|
+
|
|
29
|
+
## Pages
|
|
30
|
+
### Run `npx seomd analyze` to populate these sections
|
|
31
|
+
|
|
32
|
+
pages:
|
|
33
|
+
|
|
34
|
+
### Homepage
|
|
35
|
+
|
|
36
|
+
- id: homepage
|
|
37
|
+
url: /
|
|
38
|
+
status: planned
|
|
39
|
+
citation_rate: null
|
|
40
|
+
gap_score: null
|
|
41
|
+
top_cited_competitor: null
|
|
42
|
+
|
|
43
|
+
why_page_won: []
|
|
44
|
+
citation_hooks: []
|
|
45
|
+
|
|
46
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
47
|
+
|
|
48
|
+
remediation_playbook: []
|
|
49
|
+
|
|
50
|
+
fastest_win:
|
|
51
|
+
action: null
|
|
52
|
+
estimated_gap_score_improvement: null
|
|
53
|
+
effort: null
|
|
54
|
+
|
|
55
|
+
suggested_content_outline: []
|
|
56
|
+
|
|
57
|
+
### Category
|
|
58
|
+
|
|
59
|
+
- id: category
|
|
60
|
+
url: /[category]
|
|
61
|
+
status: planned
|
|
62
|
+
citation_rate: null
|
|
63
|
+
gap_score: null
|
|
64
|
+
top_cited_competitor: null
|
|
65
|
+
|
|
66
|
+
why_page_won: []
|
|
67
|
+
citation_hooks: []
|
|
68
|
+
|
|
69
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
70
|
+
|
|
71
|
+
remediation_playbook: []
|
|
72
|
+
|
|
73
|
+
fastest_win:
|
|
74
|
+
action: null
|
|
75
|
+
estimated_gap_score_improvement: null
|
|
76
|
+
effort: null
|
|
77
|
+
|
|
78
|
+
suggested_content_outline: []
|
|
79
|
+
|
|
80
|
+
### Article
|
|
81
|
+
|
|
82
|
+
- id: article
|
|
83
|
+
url: /[category]/[slug]
|
|
84
|
+
status: planned
|
|
85
|
+
citation_rate: null
|
|
86
|
+
gap_score: null
|
|
87
|
+
top_cited_competitor: null
|
|
88
|
+
|
|
89
|
+
why_page_won: []
|
|
90
|
+
citation_hooks: []
|
|
91
|
+
|
|
92
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
93
|
+
|
|
94
|
+
remediation_playbook: []
|
|
95
|
+
|
|
96
|
+
fastest_win:
|
|
97
|
+
action: null
|
|
98
|
+
estimated_gap_score_improvement: null
|
|
99
|
+
effort: null
|
|
100
|
+
|
|
101
|
+
suggested_content_outline: []
|
|
102
|
+
|
|
103
|
+
### Author
|
|
104
|
+
|
|
105
|
+
- id: author
|
|
106
|
+
url: /author/[slug]
|
|
107
|
+
status: planned
|
|
108
|
+
citation_rate: null
|
|
109
|
+
gap_score: null
|
|
110
|
+
top_cited_competitor: null
|
|
111
|
+
|
|
112
|
+
why_page_won: []
|
|
113
|
+
citation_hooks: []
|
|
114
|
+
|
|
115
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
116
|
+
|
|
117
|
+
remediation_playbook: []
|
|
118
|
+
|
|
119
|
+
fastest_win:
|
|
120
|
+
action: null
|
|
121
|
+
estimated_gap_score_improvement: null
|
|
122
|
+
effort: null
|
|
123
|
+
|
|
124
|
+
suggested_content_outline: []
|
|
125
|
+
|
|
126
|
+
### About
|
|
127
|
+
|
|
128
|
+
- id: about
|
|
129
|
+
url: /about
|
|
130
|
+
status: planned
|
|
131
|
+
citation_rate: null
|
|
132
|
+
gap_score: null
|
|
133
|
+
top_cited_competitor: null
|
|
134
|
+
|
|
135
|
+
why_page_won: []
|
|
136
|
+
citation_hooks: []
|
|
137
|
+
|
|
138
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
139
|
+
|
|
140
|
+
remediation_playbook: []
|
|
141
|
+
|
|
142
|
+
fastest_win:
|
|
143
|
+
action: null
|
|
144
|
+
estimated_gap_score_improvement: null
|
|
145
|
+
effort: null
|
|
146
|
+
|
|
147
|
+
suggested_content_outline: []
|
|
148
|
+
|
|
149
|
+
### Newsletter
|
|
150
|
+
|
|
151
|
+
- id: newsletter
|
|
152
|
+
url: /newsletter
|
|
153
|
+
status: planned
|
|
154
|
+
citation_rate: null
|
|
155
|
+
gap_score: null
|
|
156
|
+
top_cited_competitor: null
|
|
157
|
+
|
|
158
|
+
why_page_won: []
|
|
159
|
+
citation_hooks: []
|
|
160
|
+
|
|
161
|
+
gaps_for_{{brand_lower_snake}}: []
|
|
162
|
+
|
|
163
|
+
remediation_playbook: []
|
|
164
|
+
|
|
165
|
+
fastest_win:
|
|
166
|
+
action: null
|
|
167
|
+
estimated_gap_score_improvement: null
|
|
168
|
+
effort: null
|
|
169
|
+
|
|
170
|
+
suggested_content_outline: []
|
|
171
|
+
|
|
172
|
+
## Competitor Profiles
|
|
173
|
+
### Platform populated ā competitor citation patterns
|
|
174
|
+
|
|
175
|
+
competitors:
|
|
176
|
+
{{reverse_competitors}}
|