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.
Files changed (3) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seo-intel",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "Local Ahrefs-style SEO competitor intelligence. Crawl → SQLite → cloud analysis.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
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) || prof === 'ai-pipeline') return 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
- if (prof === 'content') {
834
- // Content: only low-scoring pages (needs improvement)
835
- return data.filter(r => r.score < 60);
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
- if (prof === 'content') {
842
- // Content: only competitor-dominated keywords (role != target/owned)
843
- const byKw = {};
844
- for (const r of data) { (byKw[r.keyword] ||= []).push(r); }
845
- const gapKws = new Set();
846
- for (const [kw, rows] of Object.entries(byKw)) {
847
- const hasTarget = rows.some(r => r.role === 'target' || r.role === 'owned');
848
- const hasCompetitor = rows.some(r => r.role === 'competitor');
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
- return data;
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`;