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.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seo-intel",
3
- "version": "1.4.6",
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
@@ -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', 'schemas', 'links', 'headings', 'watch', 'insights'],
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) || 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 = [];
@@ -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
- if (prof === 'content') {
838
- // Content: only low-scoring pages (needs improvement)
839
- return data.filter(r => r.score < 60);
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
- if (prof === 'content') {
846
- // Content: only competitor-dominated keywords (role != target/owned)
847
- const byKw = {};
848
- for (const r of data) { (byKw[r.keyword] ||= []).push(r); }
849
- const gapKws = new Set();
850
- for (const [kw, rows] of Object.entries(byKw)) {
851
- const hasTarget = rows.some(r => r.role === 'target' || r.role === 'owned');
852
- const hasCompetitor = rows.some(r => r.role === 'competitor');
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
- 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);
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`;