seo-intel 1.4.7 → 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 +8 -0
- package/package.json +1 -1
- package/server.js +30 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 1.4.7 (2026-04-09)
|
|
4
12
|
|
|
5
13
|
### Export: profiles are actions only
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -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 = [];
|
|
@@ -830,27 +830,30 @@ async function handleRequest(req, res) {
|
|
|
830
830
|
case 'schemas': return data; // raw only — not in any profile
|
|
831
831
|
case 'aeo': {
|
|
832
832
|
if (!Array.isArray(data)) return data;
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
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);
|
|
838
836
|
}
|
|
839
837
|
case 'keywords': {
|
|
840
838
|
if (!Array.isArray(data)) return data;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (!hasTarget && hasCompetitor) gapKws.add(kw);
|
|
850
|
-
}
|
|
851
|
-
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);
|
|
852
847
|
}
|
|
853
|
-
|
|
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);
|
|
854
857
|
}
|
|
855
858
|
case 'watch': {
|
|
856
859
|
// Keep only errors + warnings, drop notices
|
|
@@ -954,6 +957,15 @@ async function handleRequest(req, res) {
|
|
|
954
957
|
return md;
|
|
955
958
|
}
|
|
956
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
|
+
}
|
|
957
969
|
let md = header + '## Keyword Matrix\n\n| Keyword | Domain | Role | Location | Frequency |\n|---------|--------|------|----------|-----------|\n';
|
|
958
970
|
for (const r of data.slice(0, 500)) {
|
|
959
971
|
md += `| ${r.keyword} | ${r.domain} | ${r.role} | ${r.location || ''} | ${r.freq} |\n`;
|