seo-intel 1.4.4 → 1.4.5

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 +7 -0
  2. package/package.json +1 -1
  3. package/server.js +37 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.5 (2026-04-09)
4
+
5
+ ### Export: actionable summaries only
6
+ - Technical export: per-page issue summary (own site only) — lists specific problems per URL
7
+ - Links export: per-page link issue summary (own site only) — orphan pages, missing anchors, excessive external links
8
+ - No more raw data dumps in profile exports — every row is an action item
9
+
3
10
  ## 1.4.4 (2026-04-08)
4
11
 
5
12
  ### Export Profiles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seo-intel",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
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
@@ -759,12 +759,24 @@ async function handleRequest(req, res) {
759
759
  }
760
760
  case 'technical': {
761
761
  if (!Array.isArray(data) || prof === 'ai-pipeline') return data;
762
- // Dev profile: only pages with issues
763
- return data.filter(r =>
764
- r.status_code >= 400 || !r.has_canonical || !r.has_og_tags ||
765
- !r.has_schema || !r.has_robots || !r.is_mobile_ok ||
766
- (r.load_ms && r.load_ms > 3000) || (r.word_count != null && r.word_count < 100)
767
- );
762
+ // Own site only, per-page issue summary
763
+ const own = data.filter(r => r.role === 'target' || r.role === 'owned');
764
+ const issues = [];
765
+ for (const r of own) {
766
+ const problems = [];
767
+ if (r.status_code >= 400) problems.push(`HTTP ${r.status_code}`);
768
+ if (!r.has_canonical) problems.push('no canonical');
769
+ if (!r.has_og_tags) problems.push('no OG tags');
770
+ if (!r.has_schema) problems.push('no schema');
771
+ if (!r.has_robots) problems.push('no robots meta');
772
+ if (!r.is_mobile_ok) problems.push('not mobile-friendly');
773
+ if (r.load_ms && r.load_ms > 3000) problems.push(`slow (${r.load_ms}ms)`);
774
+ if (r.word_count != null && r.word_count < 100) problems.push(`thin content (${r.word_count} words)`);
775
+ if (problems.length) {
776
+ issues.push({ url: r.url, domain: r.domain, issues: problems.join(', '), status: r.status_code, load_ms: r.load_ms, word_count: r.word_count });
777
+ }
778
+ }
779
+ return issues;
768
780
  }
769
781
  case 'headings': {
770
782
  if (!Array.isArray(data)) return data;
@@ -795,12 +807,25 @@ async function handleRequest(req, res) {
795
807
  }
796
808
  case 'links': {
797
809
  if (!Array.isArray(data)) return data;
798
- // Only orphan pages (pages that are never a target) and broken anchors
799
- const targetUrls = new Set(data.filter(l => l.is_internal).map(l => l.target_url));
800
- const sourceUrls = new Set(data.map(l => l.source_url));
801
- // Pages that link out but are never linked TO = orphan
802
- const orphans = new Set([...sourceUrls].filter(u => !targetUrls.has(u)));
803
- return data.filter(r => orphans.has(r.source_url) || !r.anchor_text);
810
+ // Own site only, summarize to per-page link issues
811
+ const ownLinks = data.filter(r => r.role === 'target' || r.role === 'owned');
812
+ const internalTargets = new Set(ownLinks.filter(l => l.is_internal).map(l => l.target_url));
813
+ const byPage = {};
814
+ for (const r of ownLinks) (byPage[r.source_url] ||= []).push(r);
815
+ const issues = [];
816
+ for (const [url, links] of Object.entries(byPage)) {
817
+ const problems = [];
818
+ const noAnchor = links.filter(l => !l.anchor_text);
819
+ if (noAnchor.length) problems.push(`${noAnchor.length} links missing anchor text`);
820
+ if (!internalTargets.has(url)) problems.push('orphan page (no internal links point here)');
821
+ const extLinks = links.filter(l => !l.is_internal);
822
+ // flag if page has excessive external links
823
+ if (extLinks.length > 20) problems.push(`${extLinks.length} external links`);
824
+ if (problems.length) {
825
+ issues.push({ url, domain: links[0].domain, issues: problems.join(', '), total_links: links.length, internal: links.filter(l => l.is_internal).length, external: extLinks.length });
826
+ }
827
+ }
828
+ return issues;
804
829
  }
805
830
  case 'schemas': {
806
831
  if (!Array.isArray(data) || prof !== 'dev') return data;