seo-intel 1.5.1 → 1.5.2
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/reports/generate-html.js +2 -8
- package/server.js +72 -120
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.2 (2026-04-11)
|
|
4
|
+
|
|
5
|
+
### Unified Export
|
|
6
|
+
- Merged dev/content/ai-pipeline profiles into a single unified export
|
|
7
|
+
- One file, all actionable sections: scorecard → fixes → content strategy → reference
|
|
8
|
+
- Removed profile picker — just choose format (MD/JSON/CSV/ZIP) and download
|
|
9
|
+
- Cleaner filenames: `carbium-2026-04-11.md` instead of `carbium-dev-2026-04-11.md`
|
|
10
|
+
|
|
3
11
|
## 1.5.1 (2026-04-11)
|
|
4
12
|
|
|
5
13
|
### Setup Wizard
|
package/package.json
CHANGED
package/reports/generate-html.js
CHANGED
|
@@ -2214,11 +2214,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2214
2214
|
<div class="profile-export-picker" id="profilePicker${suffix}">
|
|
2215
2215
|
<button class="export-btn profile-export-trigger"><i class="fa-solid fa-download"></i> Export Report <i class="fa-solid fa-chevron-down" style="font-size:0.55rem;margin-left:auto;opacity:0.5;"></i></button>
|
|
2216
2216
|
<div class="profile-export-menu">
|
|
2217
|
-
<div class="draft-menu-section">
|
|
2218
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="dev" /> <i class="fa-solid fa-wrench"></i> Developer <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">technical fixes, schema gaps</span></label>
|
|
2219
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="content" checked /> <i class="fa-solid fa-pen-fancy"></i> Content <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">keyword gaps, opportunities</span></label>
|
|
2220
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="ai-pipeline" /> <i class="fa-solid fa-robot"></i> AI Pipeline <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">structured JSON for LLMs</span></label>
|
|
2221
|
-
<div class="draft-menu-section" style="margin-top:8px;">Format</div>
|
|
2217
|
+
<div class="draft-menu-section">Format</div>
|
|
2222
2218
|
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
2223
2219
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="md" checked /> <i class="fa-solid fa-file-lines"></i> MD</label>
|
|
2224
2220
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="json" /> <i class="fa-solid fa-code"></i> JSON</label>
|
|
@@ -2649,13 +2645,11 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2649
2645
|
if (picker2) {
|
|
2650
2646
|
e.stopImmediatePropagation();
|
|
2651
2647
|
var projP = profDl.getAttribute('data-project');
|
|
2652
|
-
var profVal = picker2.querySelector('input[name^="exportProfile"]:checked');
|
|
2653
2648
|
var fmtVal = picker2.querySelector('input[name^="exportFmt"]:checked');
|
|
2654
|
-
var prof = profVal ? profVal.value : 'content';
|
|
2655
2649
|
var fmt2 = fmtVal ? fmtVal.value : 'md';
|
|
2656
2650
|
picker2.querySelector('.profile-export-menu').style.display = 'none';
|
|
2657
2651
|
if (window.location.protocol.startsWith('http')) {
|
|
2658
|
-
window.location = '/api/export/download?project=' + encodeURIComponent(projP) + '&
|
|
2652
|
+
window.location = '/api/export/download?project=' + encodeURIComponent(projP) + '&format=' + encodeURIComponent(fmt2);
|
|
2659
2653
|
}
|
|
2660
2654
|
return;
|
|
2661
2655
|
}
|
package/server.js
CHANGED
|
@@ -594,7 +594,6 @@ async function handleRequest(req, res) {
|
|
|
594
594
|
const project = url.searchParams.get('project');
|
|
595
595
|
const section = url.searchParams.get('section') || 'all';
|
|
596
596
|
const format = url.searchParams.get('format') || 'json';
|
|
597
|
-
const profile = url.searchParams.get('profile'); // dev | content | ai-pipeline
|
|
598
597
|
|
|
599
598
|
if (!project || !/^[a-z0-9_-]+$/i.test(project)) { json(res, 400, { error: 'Invalid project name' }); return; }
|
|
600
599
|
|
|
@@ -611,111 +610,73 @@ async function handleRequest(req, res) {
|
|
|
611
610
|
// ── Gather dashboard data — same source as the HTML dashboard ──
|
|
612
611
|
const dash = gatherProjectData(db, project, config);
|
|
613
612
|
|
|
614
|
-
// ── Build export from dashboard data
|
|
615
|
-
function buildDashboardExport(dash
|
|
613
|
+
// ── Build unified export from dashboard data ──
|
|
614
|
+
function buildDashboardExport(dash) {
|
|
616
615
|
const a = dash.latestAnalysis || {};
|
|
617
616
|
const sections = {};
|
|
618
617
|
|
|
619
|
-
// ──
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
618
|
+
// ── Status: scorecard + crawl ──
|
|
619
|
+
const target = dash.technicalScores?.find(d => d.isTarget);
|
|
620
|
+
if (target) {
|
|
621
|
+
sections.technical = {
|
|
622
|
+
score: target.score,
|
|
623
|
+
h1_coverage: target.h1Pct + '%',
|
|
624
|
+
meta_coverage: target.metaPct + '%',
|
|
625
|
+
schema_coverage: target.schemaPct + '%',
|
|
626
|
+
title_coverage: target.titlePct + '%',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── Site Watch ──
|
|
631
|
+
if (dash.watchData?.events?.length) {
|
|
632
|
+
const critical = dash.watchData.events.filter(e => e.severity === 'error' || e.severity === 'warning');
|
|
633
|
+
if (critical.length) sections.watch_alerts = critical;
|
|
634
|
+
if (dash.watchData.snapshot) {
|
|
635
|
+
sections.watch_summary = {
|
|
636
|
+
health_score: dash.watchData.snapshot.health_score,
|
|
637
|
+
errors: dash.watchData.snapshot.errors_count,
|
|
638
|
+
warnings: dash.watchData.snapshot.warnings_count,
|
|
629
639
|
};
|
|
630
640
|
}
|
|
631
|
-
if (a.technical_gaps?.length) sections.technical_gaps = a.technical_gaps;
|
|
632
641
|
}
|
|
633
642
|
|
|
634
|
-
// ──
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// ──
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// ── Keyword Inventor ──
|
|
674
|
-
if (!prof || prof === 'content' || prof === 'ai-pipeline') {
|
|
675
|
-
if (a.keyword_inventor?.length) sections.keyword_inventor = a.keyword_inventor;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// ── Positioning Strategy ──
|
|
679
|
-
if (!prof || prof === 'content' || prof === 'ai-pipeline') {
|
|
680
|
-
if (a.positioning) sections.positioning = a.positioning;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// ── AI Citability (AEO) — own site, low scores only ──
|
|
684
|
-
if (!prof || prof === 'content' || prof === 'ai-pipeline') {
|
|
685
|
-
if (dash.citabilityData?.scores?.length) {
|
|
686
|
-
const own = dash.citabilityData.scores.filter(s => s.role === 'target' || s.role === 'owned');
|
|
687
|
-
const needsWork = own.filter(s => s.score < 60);
|
|
688
|
-
if (needsWork.length) sections.citability_low_scores = needsWork;
|
|
689
|
-
sections.citability_summary = {
|
|
690
|
-
avg_score: own.length ? Math.round(own.reduce((a, s) => a + s.score, 0) / own.length) : null,
|
|
691
|
-
pages_scored: own.length,
|
|
692
|
-
pages_below_60: needsWork.length,
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// ── Site Watch — errors and warnings only ──
|
|
698
|
-
if (!prof || prof === 'dev' || prof === 'ai-pipeline') {
|
|
699
|
-
if (dash.watchData?.events?.length) {
|
|
700
|
-
const critical = dash.watchData.events.filter(e => e.severity === 'error' || e.severity === 'warning');
|
|
701
|
-
if (critical.length) sections.watch_alerts = critical;
|
|
702
|
-
if (dash.watchData.snapshot) {
|
|
703
|
-
sections.watch_summary = {
|
|
704
|
-
health_score: dash.watchData.snapshot.health_score,
|
|
705
|
-
errors: dash.watchData.snapshot.errors_count,
|
|
706
|
-
warnings: dash.watchData.snapshot.warnings_count,
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// ── Schema Breakdown — own site type counts, not per-page dump ──
|
|
713
|
-
if (!prof || prof === 'dev') {
|
|
714
|
-
if (dash.schemaBreakdown?.length) {
|
|
715
|
-
const target = dash.schemaBreakdown.find(d => d.isTarget);
|
|
716
|
-
if (target?.types?.length) sections.schema_types = target.types;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
643
|
+
// ── Fix Now: technical gaps + quick wins ──
|
|
644
|
+
if (a.technical_gaps?.length) sections.technical_gaps = a.technical_gaps;
|
|
645
|
+
if (a.quick_wins?.length) sections.quick_wins = a.quick_wins;
|
|
646
|
+
|
|
647
|
+
// ── Content Strategy: keywords, gaps, new pages, positioning ──
|
|
648
|
+
if (a.keyword_gaps?.length) sections.keyword_gaps = a.keyword_gaps;
|
|
649
|
+
if (dash.keywordGaps?.length) sections.top_keyword_gaps = dash.keywordGaps.slice(0, 50);
|
|
650
|
+
if (a.long_tails?.length) sections.long_tails = a.long_tails;
|
|
651
|
+
if (a.new_pages?.length) sections.new_pages = a.new_pages;
|
|
652
|
+
if (a.content_gaps?.length) sections.content_gaps = a.content_gaps;
|
|
653
|
+
if (a.positioning) sections.positioning = a.positioning;
|
|
654
|
+
|
|
655
|
+
// ── AI Citability ──
|
|
656
|
+
if (dash.citabilityData?.scores?.length) {
|
|
657
|
+
const own = dash.citabilityData.scores.filter(s => s.role === 'target' || s.role === 'owned');
|
|
658
|
+
const needsWork = own.filter(s => s.score < 60);
|
|
659
|
+
if (needsWork.length) sections.citability_low_scores = needsWork;
|
|
660
|
+
sections.citability_summary = {
|
|
661
|
+
avg_score: own.length ? Math.round(own.reduce((a, s) => a + s.score, 0) / own.length) : null,
|
|
662
|
+
pages_scored: own.length,
|
|
663
|
+
pages_below_60: needsWork.length,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── Reference: internal links, schema types, keyword ideas ──
|
|
668
|
+
if (dash.internalLinks) {
|
|
669
|
+
sections.internal_links = {
|
|
670
|
+
total_links: dash.internalLinks.totalLinks,
|
|
671
|
+
orphan_pages: dash.internalLinks.orphanCount,
|
|
672
|
+
top_pages: dash.internalLinks.topPages,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (dash.schemaBreakdown?.length) {
|
|
676
|
+
const tgt = dash.schemaBreakdown.find(d => d.isTarget);
|
|
677
|
+
if (tgt?.types?.length) sections.schema_types = tgt.types;
|
|
678
|
+
}
|
|
679
|
+
if (a.keyword_inventor?.length) sections.keyword_inventor = a.keyword_inventor;
|
|
719
680
|
|
|
720
681
|
// ── Crawl Stats ──
|
|
721
682
|
sections.crawl_stats = dash.crawlStats;
|
|
@@ -723,10 +684,9 @@ async function handleRequest(req, res) {
|
|
|
723
684
|
return sections;
|
|
724
685
|
}
|
|
725
686
|
|
|
726
|
-
function dashboardToMarkdown(sections, proj
|
|
687
|
+
function dashboardToMarkdown(sections, proj) {
|
|
727
688
|
const date = new Date().toISOString().slice(0, 10);
|
|
728
|
-
|
|
729
|
-
let md = `# SEO Intel — ${label} Report\n\n- Project: ${proj}\n- Date: ${date}\n\n`;
|
|
689
|
+
let md = `# SEO Intel Report — ${proj}\n\n- Date: ${date}\n\n`;
|
|
730
690
|
|
|
731
691
|
const s = sections;
|
|
732
692
|
|
|
@@ -841,39 +801,31 @@ async function handleRequest(req, res) {
|
|
|
841
801
|
}
|
|
842
802
|
|
|
843
803
|
// ── Build and serve ──
|
|
844
|
-
const
|
|
845
|
-
if (profile && !validProfiles.includes(profile)) {
|
|
846
|
-
json(res, 400, { error: `Invalid profile. Allowed: ${validProfiles.join(', ')}` });
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const sections = buildDashboardExport(dash, profile);
|
|
851
|
-
const profileLabel = profile ? { dev: 'Developer', content: 'Content', 'ai-pipeline': 'AI Pipeline' }[profile] : 'Full';
|
|
852
|
-
const tag = profile || 'full';
|
|
804
|
+
const sections = buildDashboardExport(dash);
|
|
853
805
|
|
|
854
806
|
if (format === 'json') {
|
|
855
|
-
const content = JSON.stringify({
|
|
856
|
-
const fileName = `${project}-${
|
|
807
|
+
const content = JSON.stringify({ project, date: dateStr, ...sections }, null, 2);
|
|
808
|
+
const fileName = `${project}-${dateStr}.json`;
|
|
857
809
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="${fileName}"` });
|
|
858
810
|
res.end(content);
|
|
859
811
|
} else if (format === 'md') {
|
|
860
|
-
const content = dashboardToMarkdown(sections, project
|
|
861
|
-
const fileName = `${project}-${
|
|
812
|
+
const content = dashboardToMarkdown(sections, project);
|
|
813
|
+
const fileName = `${project}-${dateStr}.md`;
|
|
862
814
|
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8', 'Content-Disposition': `attachment; filename="${fileName}"` });
|
|
863
815
|
res.end(content);
|
|
864
816
|
} else if (format === 'csv') {
|
|
865
817
|
const content = toCSV(sections);
|
|
866
|
-
const fileName = `${project}-${
|
|
818
|
+
const fileName = `${project}-${dateStr}.csv`;
|
|
867
819
|
res.writeHead(200, { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': `attachment; filename="${fileName}"` });
|
|
868
820
|
res.end(content || 'No data.');
|
|
869
821
|
} else if (format === 'zip') {
|
|
870
822
|
const entries = [];
|
|
871
|
-
entries.push({ name: `${project}-${
|
|
872
|
-
entries.push({ name: `${project}-${
|
|
823
|
+
entries.push({ name: `${project}-${dateStr}.json`, content: JSON.stringify({ project, date: dateStr, ...sections }, null, 2) });
|
|
824
|
+
entries.push({ name: `${project}-${dateStr}.md`, content: dashboardToMarkdown(sections, project) });
|
|
873
825
|
const csv = toCSV(sections);
|
|
874
|
-
if (csv) entries.push({ name: `${project}-${
|
|
826
|
+
if (csv) entries.push({ name: `${project}-${dateStr}.csv`, content: csv });
|
|
875
827
|
const zipBuf = createZip(entries);
|
|
876
|
-
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Disposition': `attachment; filename="${project}-${
|
|
828
|
+
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Disposition': `attachment; filename="${project}-${dateStr}.zip"`, 'Content-Length': zipBuf.length });
|
|
877
829
|
res.end(zipBuf);
|
|
878
830
|
} else {
|
|
879
831
|
json(res, 400, { error: 'Invalid format. Allowed: json, csv, md, zip' });
|