seo-intel 1.4.6 → 1.4.8
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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/server.js +32 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.8 (2026-04-10)
|
|
4
|
+
|
|
5
|
+
### Export: own site only, zero competitor bloat
|
|
6
|
+
- ALL profile sections now filter to own site (target/owned) — no competitor pages, links, headings, or AEO scores
|
|
7
|
+
- Keywords export shows gap summary only: keywords competitors use that you don't, with who uses them
|
|
8
|
+
- AEO export shows only low-scoring own pages (<60) that need improvement
|
|
9
|
+
- Technical export was already own-site; removed the AI pipeline exception that bypassed filtering
|
|
10
|
+
|
|
11
|
+
## 1.4.7 (2026-04-09)
|
|
12
|
+
|
|
13
|
+
### Export: profiles are actions only
|
|
14
|
+
- Removed schemas from all export profiles — pure inventory, not actionable
|
|
15
|
+
- "No schema" issues already surfaced in technical section
|
|
16
|
+
- Raw Full Export (ZIP) still includes everything for data access
|
|
17
|
+
|
|
3
18
|
## 1.4.6 (2026-04-09)
|
|
4
19
|
|
|
5
20
|
### Export: rich actionable content
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -611,7 +611,7 @@ async function handleRequest(req, res) {
|
|
|
611
611
|
// ── Profile definitions: which sections + which insight types matter ──
|
|
612
612
|
const PROFILES = {
|
|
613
613
|
dev: {
|
|
614
|
-
sections: ['technical', '
|
|
614
|
+
sections: ['technical', 'links', 'headings', 'watch', 'insights'],
|
|
615
615
|
insightTypes: ['technical_gap', 'quick_win', 'site_watch'],
|
|
616
616
|
label: 'Developer',
|
|
617
617
|
},
|
|
@@ -758,7 +758,7 @@ async function handleRequest(req, res) {
|
|
|
758
758
|
return p.insightTypes ? data.filter(r => p.insightTypes.includes(r._type)) : data;
|
|
759
759
|
}
|
|
760
760
|
case 'technical': {
|
|
761
|
-
if (!Array.isArray(data)
|
|
761
|
+
if (!Array.isArray(data)) return data;
|
|
762
762
|
// Own site only, per-page issue summary
|
|
763
763
|
const own = data.filter(r => r.role === 'target' || r.role === 'owned');
|
|
764
764
|
const issues = [];
|
|
@@ -827,34 +827,33 @@ async function handleRequest(req, res) {
|
|
|
827
827
|
}
|
|
828
828
|
return issues;
|
|
829
829
|
}
|
|
830
|
-
case 'schemas':
|
|
831
|
-
if (!Array.isArray(data)) return data;
|
|
832
|
-
// Own site only for all profiles
|
|
833
|
-
return data.filter(r => r.role === 'target' || r.role === 'owned');
|
|
834
|
-
}
|
|
830
|
+
case 'schemas': return data; // raw only — not in any profile
|
|
835
831
|
case 'aeo': {
|
|
836
832
|
if (!Array.isArray(data)) return data;
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
return data;
|
|
833
|
+
// Own site only, low-scoring pages that need work
|
|
834
|
+
const ownAeo = data.filter(r => r.role === 'target' || r.role === 'owned');
|
|
835
|
+
return ownAeo.filter(r => r.score < 60);
|
|
842
836
|
}
|
|
843
837
|
case 'keywords': {
|
|
844
838
|
if (!Array.isArray(data)) return data;
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (!hasTarget && hasCompetitor) gapKws.add(kw);
|
|
854
|
-
}
|
|
855
|
-
return data.filter(r => gapKws.has(r.keyword));
|
|
839
|
+
// Only keyword gaps: competitor has it, you don't
|
|
840
|
+
const byKw = {};
|
|
841
|
+
for (const r of data) { (byKw[r.keyword] ||= []).push(r); }
|
|
842
|
+
const gapKws = new Set();
|
|
843
|
+
for (const [kw, rows] of Object.entries(byKw)) {
|
|
844
|
+
const hasTarget = rows.some(r => r.role === 'target' || r.role === 'owned');
|
|
845
|
+
const hasCompetitor = rows.some(r => r.role === 'competitor');
|
|
846
|
+
if (!hasTarget && hasCompetitor) gapKws.add(kw);
|
|
856
847
|
}
|
|
857
|
-
|
|
848
|
+
// Return gap keywords with which competitors use them
|
|
849
|
+
const gaps = [];
|
|
850
|
+
for (const kw of gapKws) {
|
|
851
|
+
const rows = byKw[kw];
|
|
852
|
+
const competitors = rows.map(r => r.domain).join(', ');
|
|
853
|
+
const topFreq = Math.max(...rows.map(r => r.freq));
|
|
854
|
+
gaps.push({ keyword: kw, used_by: competitors, frequency: topFreq });
|
|
855
|
+
}
|
|
856
|
+
return gaps.sort((a, b) => b.frequency - a.frequency);
|
|
858
857
|
}
|
|
859
858
|
case 'watch': {
|
|
860
859
|
// Keep only errors + warnings, drop notices
|
|
@@ -958,6 +957,15 @@ async function handleRequest(req, res) {
|
|
|
958
957
|
return md;
|
|
959
958
|
}
|
|
960
959
|
case 'keywords': {
|
|
960
|
+
// Profile exports return gap summary; raw exports return full matrix
|
|
961
|
+
if (data[0] && data[0].used_by !== undefined) {
|
|
962
|
+
let md = header + `## Keyword Gaps (${data.length})\n\nKeywords competitors use that you don't.\n\n| Keyword | Used By | Frequency |\n|---------|---------|----------|\n`;
|
|
963
|
+
for (const r of data.slice(0, 200)) {
|
|
964
|
+
md += `| ${r.keyword} | ${r.used_by} | ${r.frequency} |\n`;
|
|
965
|
+
}
|
|
966
|
+
if (data.length > 200) md += `\n_...and ${data.length - 200} more._\n`;
|
|
967
|
+
return md;
|
|
968
|
+
}
|
|
961
969
|
let md = header + '## Keyword Matrix\n\n| Keyword | Domain | Role | Location | Frequency |\n|---------|--------|------|----------|-----------|\n';
|
|
962
970
|
for (const r of data.slice(0, 500)) {
|
|
963
971
|
md += `| ${r.keyword} | ${r.domain} | ${r.role} | ${r.location || ''} | ${r.freq} |\n`;
|