seo-intel 1.5.39 → 1.5.45
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 +76 -0
- package/analyses/blog-draft/prescorer.js +17 -0
- package/analyses/loop/orchestrator.js +179 -0
- package/cli.js +197 -6
- package/crawler/html-extract.js +127 -0
- package/crawler/light.js +169 -0
- package/db/db.js +66 -0
- package/lib/cron.js +108 -0
- package/lib/gate.js +33 -1
- package/lib/intel.js +9 -3
- package/mcp/server.js +172 -17
- package/package.json +1 -1
- package/reports/generate-html.js +42 -404
- package/setup/web-routes.js +39 -0
- package/setup/wizard.html +73 -0
package/reports/generate-html.js
CHANGED
|
@@ -225,6 +225,12 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
225
225
|
const suffix = opts.suffix || ''; // '' for single-project, '-projectname' for multi
|
|
226
226
|
const panelOnly = opts.panelOnly || false; // true = return body panel only (no html/head)
|
|
227
227
|
const pro = isPro();
|
|
228
|
+
// v1.5.41 monetization line — analysis of YOUR OWN site is FREE (a smart
|
|
229
|
+
// agent commoditizes one-shot analysis anyway). The paywall sits on what an
|
|
230
|
+
// agent structurally can't do for itself: competitor synthesis and history.
|
|
231
|
+
// These semantic flags make the intent explicit at every render site.
|
|
232
|
+
const showCompetitor = pro; // competitor-vs-target sections (gap, venn, battleground…)
|
|
233
|
+
const showHistory = pro; // over-time / "what changed" trend sections
|
|
228
234
|
|
|
229
235
|
const {
|
|
230
236
|
project, targetDomain, competitorDomains, allDomains,
|
|
@@ -250,7 +256,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
250
256
|
<head>
|
|
251
257
|
<meta charset="UTF-8">
|
|
252
258
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
253
|
-
<title>SEO Intel
|
|
259
|
+
<title>SEO Intel Dashboard — ${project.toUpperCase()}</title>
|
|
254
260
|
<link rel="icon" type="image/png" href="/favicon.png?v=${Date.now()}">
|
|
255
261
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
256
262
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -1883,361 +1889,6 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1883
1889
|
</style>
|
|
1884
1890
|
</head>`;
|
|
1885
1891
|
|
|
1886
|
-
// ── Free tier: structural audit dashboard (crawl data only, no extractions/analysis) ──
|
|
1887
|
-
if (!pro) {
|
|
1888
|
-
// All queries use only: pages, technical, links, headings, page_schemas
|
|
1889
|
-
// No joins to extractions or analyses tables.
|
|
1890
|
-
|
|
1891
|
-
const targetPages = (() => {
|
|
1892
|
-
try {
|
|
1893
|
-
return db.prepare(`
|
|
1894
|
-
SELECT p.url, p.status_code, p.word_count, p.load_ms, p.click_depth, p.is_indexable,
|
|
1895
|
-
h.text as h1,
|
|
1896
|
-
t.has_canonical, t.has_og_tags, t.has_schema
|
|
1897
|
-
FROM pages p
|
|
1898
|
-
JOIN domains d ON d.id = p.domain_id
|
|
1899
|
-
LEFT JOIN (
|
|
1900
|
-
SELECT page_id, MIN(text) as text FROM headings WHERE level = 1 GROUP BY page_id
|
|
1901
|
-
) h ON h.page_id = p.id
|
|
1902
|
-
LEFT JOIN technical t ON t.page_id = p.id
|
|
1903
|
-
WHERE d.project = ? AND d.role = 'target'
|
|
1904
|
-
ORDER BY p.click_depth, p.url LIMIT 100
|
|
1905
|
-
`).all(project);
|
|
1906
|
-
} catch { return []; }
|
|
1907
|
-
})();
|
|
1908
|
-
|
|
1909
|
-
const techCoverage = (() => {
|
|
1910
|
-
try {
|
|
1911
|
-
return db.prepare(`
|
|
1912
|
-
SELECT
|
|
1913
|
-
COUNT(*) as total,
|
|
1914
|
-
SUM(t.has_canonical) as canonical,
|
|
1915
|
-
SUM(t.has_og_tags) as og,
|
|
1916
|
-
SUM(t.has_schema) as schema,
|
|
1917
|
-
SUM(t.has_robots) as robots
|
|
1918
|
-
FROM technical t
|
|
1919
|
-
JOIN pages p ON p.id = t.page_id
|
|
1920
|
-
JOIN domains d ON d.id = p.domain_id
|
|
1921
|
-
WHERE d.project = ? AND d.role = 'target'
|
|
1922
|
-
`).get(project) || {};
|
|
1923
|
-
} catch { return {}; }
|
|
1924
|
-
})();
|
|
1925
|
-
|
|
1926
|
-
const h1Stats = (() => {
|
|
1927
|
-
try {
|
|
1928
|
-
const total = db.prepare(`
|
|
1929
|
-
SELECT COUNT(*) as c FROM pages p JOIN domains d ON d.id = p.domain_id
|
|
1930
|
-
WHERE d.project = ? AND d.role = 'target' AND p.is_indexable = 1
|
|
1931
|
-
`).get(project)?.c || 0;
|
|
1932
|
-
const withH1 = db.prepare(`
|
|
1933
|
-
SELECT COUNT(DISTINCT p.id) as c FROM pages p
|
|
1934
|
-
JOIN domains d ON d.id = p.domain_id
|
|
1935
|
-
JOIN headings h ON h.page_id = p.id AND h.level = 1
|
|
1936
|
-
WHERE d.project = ? AND d.role = 'target' AND p.is_indexable = 1
|
|
1937
|
-
`).get(project)?.c || 0;
|
|
1938
|
-
return { total, withH1, withoutH1: total - withH1 };
|
|
1939
|
-
} catch { return { total: 0, withH1: 0, withoutH1: 0 }; }
|
|
1940
|
-
})();
|
|
1941
|
-
|
|
1942
|
-
const topLinkedPages = (() => {
|
|
1943
|
-
try {
|
|
1944
|
-
return db.prepare(`
|
|
1945
|
-
SELECT l.target_url, COUNT(*) as inbound
|
|
1946
|
-
FROM links l
|
|
1947
|
-
WHERE l.is_internal = 1
|
|
1948
|
-
AND l.source_id IN (
|
|
1949
|
-
SELECT p.id FROM pages p JOIN domains d ON d.id = p.domain_id
|
|
1950
|
-
WHERE d.project = ? AND d.role = 'target'
|
|
1951
|
-
)
|
|
1952
|
-
GROUP BY l.target_url
|
|
1953
|
-
ORDER BY inbound DESC LIMIT 15
|
|
1954
|
-
`).all(project);
|
|
1955
|
-
} catch { return []; }
|
|
1956
|
-
})();
|
|
1957
|
-
|
|
1958
|
-
const orphanPages = (() => {
|
|
1959
|
-
try {
|
|
1960
|
-
return db.prepare(`
|
|
1961
|
-
SELECT p.url FROM pages p
|
|
1962
|
-
JOIN domains d ON d.id = p.domain_id
|
|
1963
|
-
LEFT JOIN links l ON l.target_url = p.url AND l.is_internal = 1
|
|
1964
|
-
WHERE d.project = ? AND d.role = 'target' AND p.is_indexable = 1 AND l.id IS NULL
|
|
1965
|
-
LIMIT 30
|
|
1966
|
-
`).all(project);
|
|
1967
|
-
} catch { return []; }
|
|
1968
|
-
})();
|
|
1969
|
-
|
|
1970
|
-
const schemaTypes = (() => {
|
|
1971
|
-
try {
|
|
1972
|
-
return db.prepare(`
|
|
1973
|
-
SELECT ps.schema_type, COUNT(*) as count FROM page_schemas ps
|
|
1974
|
-
JOIN pages p ON ps.page_id = p.id
|
|
1975
|
-
JOIN domains d ON d.id = p.domain_id
|
|
1976
|
-
WHERE d.project = ? AND d.role = 'target'
|
|
1977
|
-
GROUP BY ps.schema_type ORDER BY count DESC
|
|
1978
|
-
`).all(project);
|
|
1979
|
-
} catch { return []; }
|
|
1980
|
-
})();
|
|
1981
|
-
|
|
1982
|
-
const allTargetPages = targetPages;
|
|
1983
|
-
const indexedPages = allTargetPages.filter(p => p.is_indexable).length;
|
|
1984
|
-
const errorPages = allTargetPages.filter(p => (p.status_code || 0) >= 400).length;
|
|
1985
|
-
const deepPages = allTargetPages.filter(p => (p.click_depth || 0) > 3).length;
|
|
1986
|
-
const missingCanonical = techCoverage.total
|
|
1987
|
-
? techCoverage.total - (techCoverage.canonical || 0) : 0;
|
|
1988
|
-
const avgWordCount = allTargetPages.length
|
|
1989
|
-
? Math.round(allTargetPages.reduce((s, p) => s + (p.word_count || 0), 0) / allTargetPages.length)
|
|
1990
|
-
: 0;
|
|
1991
|
-
const avgLoad = allTargetPages.length
|
|
1992
|
-
? Math.round(allTargetPages.reduce((s, p) => s + (p.load_ms || 0), 0) / allTargetPages.length)
|
|
1993
|
-
: 0;
|
|
1994
|
-
|
|
1995
|
-
const issues = errorPages + h1Stats.withoutH1 + (missingCanonical > (techCoverage.total || 1) * 0.2 ? 1 : 0) + deepPages;
|
|
1996
|
-
|
|
1997
|
-
const HIGH_VALUE_SCHEMA = [
|
|
1998
|
-
{ type: 'FAQPage', label: 'FAQPage', benefit: 'SERP accordion eligibility' },
|
|
1999
|
-
{ type: 'BreadcrumbList', label: 'BreadcrumbList', benefit: 'Breadcrumb rich results' },
|
|
2000
|
-
{ type: 'Organization', label: 'Organization', benefit: 'Knowledge panel' },
|
|
2001
|
-
{ type: 'Product', label: 'Product', benefit: 'Price/rating in search results' },
|
|
2002
|
-
{ type: 'Article', label: 'Article', benefit: 'News/blog rich results' },
|
|
2003
|
-
];
|
|
2004
|
-
const foundSchemaTypes = new Set(schemaTypes.map(s => s.schema_type));
|
|
2005
|
-
|
|
2006
|
-
const panelHtml = `
|
|
2007
|
-
<div class="project-panel" data-project="${project}">
|
|
2008
|
-
<div style="max-width:var(--max-width);margin:0 auto;">
|
|
2009
|
-
|
|
2010
|
-
<!-- HEADER -->
|
|
2011
|
-
<div class="header-bar" id="header">
|
|
2012
|
-
<div class="header-left">
|
|
2013
|
-
<h1>SEO Intel <span style="font-size:0.5em;color:var(--text-muted);font-weight:400;vertical-align:middle;">Structural Audit</span></h1>
|
|
2014
|
-
<div class="subtitle">Project: ${project.toUpperCase()} | Target: ${targetDomain}</div>
|
|
2015
|
-
</div>
|
|
2016
|
-
<div class="header-badges">
|
|
2017
|
-
<span class="status-badge" style="background:rgba(139,189,217,0.12);color:var(--color-info);border:1px solid rgba(139,189,217,0.2);">Free Tier</span>
|
|
2018
|
-
<span class="status-badge gold">Last Crawl: ${lastCrawl}</span>
|
|
2019
|
-
</div>
|
|
2020
|
-
</div>
|
|
2021
|
-
|
|
2022
|
-
<!-- SUMMARY CARDS -->
|
|
2023
|
-
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin:16px 0;">
|
|
2024
|
-
<div style="background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);padding:16px 12px;text-align:center;">
|
|
2025
|
-
<div style="font-family:var(--font-display);font-size:1.4rem;color:var(--text-primary);">${indexedPages}</div>
|
|
2026
|
-
<div style="font-size:0.65rem;color:var(--text-muted);">Pages Indexed</div>
|
|
2027
|
-
</div>
|
|
2028
|
-
<div style="background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);padding:16px 12px;text-align:center;">
|
|
2029
|
-
<div style="font-family:var(--font-display);font-size:1.4rem;color:${errorPages > 0 ? 'var(--color-danger)' : 'var(--color-success)'};">${errorPages}</div>
|
|
2030
|
-
<div style="font-size:0.65rem;color:var(--text-muted);">Error Pages</div>
|
|
2031
|
-
</div>
|
|
2032
|
-
<div style="background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);padding:16px 12px;text-align:center;">
|
|
2033
|
-
<div style="font-family:var(--font-display);font-size:1.4rem;color:${h1Stats.withoutH1 > 0 ? 'var(--color-warning)' : 'var(--color-success)'};">${h1Stats.withoutH1}</div>
|
|
2034
|
-
<div style="font-size:0.65rem;color:var(--text-muted);">Missing H1</div>
|
|
2035
|
-
</div>
|
|
2036
|
-
<div style="background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);padding:16px 12px;text-align:center;">
|
|
2037
|
-
<div style="font-family:var(--font-display);font-size:1.4rem;color:${avgWordCount < 300 ? 'var(--color-danger)' : avgWordCount < 800 ? 'var(--color-warning)' : 'var(--color-success)'};">${avgWordCount}</div>
|
|
2038
|
-
<div style="font-size:0.65rem;color:var(--text-muted);">Avg Words</div>
|
|
2039
|
-
</div>
|
|
2040
|
-
<div style="background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);padding:16px 12px;text-align:center;">
|
|
2041
|
-
<div style="font-family:var(--font-display);font-size:1.4rem;color:${issues > 3 ? 'var(--color-danger)' : issues > 0 ? 'var(--color-warning)' : 'var(--color-success)'};">${issues}</div>
|
|
2042
|
-
<div style="font-size:0.65rem;color:var(--text-muted);">Issues Found</div>
|
|
2043
|
-
</div>
|
|
2044
|
-
</div>
|
|
2045
|
-
|
|
2046
|
-
${issues > 0 ? `
|
|
2047
|
-
<div style="font-size:0.75rem;color:var(--text-secondary);margin-bottom:20px;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);">
|
|
2048
|
-
<i class="fa-solid fa-circle-info" style="color:var(--color-info);margin-right:6px;"></i>
|
|
2049
|
-
Crawled <strong>${totalPages}</strong> pages — found <strong>${issues} structural issue${issues !== 1 ? 's' : ''}</strong> worth reviewing.
|
|
2050
|
-
${errorPages > 0 ? `<span style="margin-left:10px;color:var(--color-danger);">● ${errorPages} error page${errorPages !== 1 ? 's' : ''}</span>` : ''}
|
|
2051
|
-
${h1Stats.withoutH1 > 0 ? `<span style="margin-left:10px;color:var(--color-warning);">● ${h1Stats.withoutH1} missing H1</span>` : ''}
|
|
2052
|
-
${deepPages > 0 ? `<span style="margin-left:10px;color:var(--color-warning);">● ${deepPages} buried deep (4+ clicks)</span>` : ''}
|
|
2053
|
-
</div>` : `
|
|
2054
|
-
<div style="font-size:0.75rem;color:var(--color-success);margin-bottom:20px;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius);">
|
|
2055
|
-
<i class="fa-solid fa-check-circle" style="margin-right:6px;"></i>
|
|
2056
|
-
Crawled <strong>${totalPages}</strong> pages — no major structural issues detected.
|
|
2057
|
-
</div>`}
|
|
2058
|
-
|
|
2059
|
-
<!-- SITE WATCH -->
|
|
2060
|
-
${watchData?.current ? buildWatchCard(watchData, escapeHtml, project) : ''}
|
|
2061
|
-
|
|
2062
|
-
<!-- PAGE INVENTORY -->
|
|
2063
|
-
<div class="card" style="margin-bottom:16px;">
|
|
2064
|
-
<h2><span class="icon"><i class="fa-solid fa-table-list"></i></span> Page Inventory — ${targetDomain}</h2>
|
|
2065
|
-
<div class="table-wrapper" style="max-height:480px;overflow-y:auto;">
|
|
2066
|
-
<table>
|
|
2067
|
-
<thead>
|
|
2068
|
-
<tr>
|
|
2069
|
-
<th>URL</th>
|
|
2070
|
-
<th>H1</th>
|
|
2071
|
-
<th style="text-align:center;">Status</th>
|
|
2072
|
-
<th style="text-align:center;">Indexed</th>
|
|
2073
|
-
<th style="text-align:right;">Depth</th>
|
|
2074
|
-
<th style="text-align:right;">Words</th>
|
|
2075
|
-
<th style="text-align:center;">Canonical</th>
|
|
2076
|
-
<th style="text-align:center;">OG</th>
|
|
2077
|
-
</tr>
|
|
2078
|
-
</thead>
|
|
2079
|
-
<tbody>
|
|
2080
|
-
${targetPages.map(p => {
|
|
2081
|
-
const shortUrl = p.url.replace(/^https?:\/\/[^/]+/, '') || '/';
|
|
2082
|
-
const statusColor = (p.status_code || 0) >= 400 ? 'var(--color-danger)' : (p.status_code || 0) >= 300 ? 'var(--color-warning)' : 'var(--color-success)';
|
|
2083
|
-
const depthColor = (p.click_depth || 0) <= 2 ? 'var(--color-success)' : (p.click_depth || 0) === 3 ? 'var(--color-warning)' : 'var(--color-danger)';
|
|
2084
|
-
const h1Text = p.h1 ? escapeHtml(p.h1.slice(0, 50)) + (p.h1.length > 50 ? '…' : '') : '<span style="color:var(--color-warning);">missing</span>';
|
|
2085
|
-
return `<tr>
|
|
2086
|
-
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.72rem;" title="${escapeHtml(p.url)}">${escapeHtml(shortUrl)}</td>
|
|
2087
|
-
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.7rem;color:var(--text-secondary);">${h1Text}</td>
|
|
2088
|
-
<td style="text-align:center;color:${statusColor};font-size:0.72rem;">${p.status_code || '—'}</td>
|
|
2089
|
-
<td style="text-align:center;font-size:0.72rem;">${p.is_indexable ? '<span style="color:var(--color-success);">✓</span>' : '<span style="color:var(--color-danger);">✗</span>'}</td>
|
|
2090
|
-
<td style="text-align:right;font-size:0.72rem;color:${depthColor};">${p.click_depth ?? '—'}</td>
|
|
2091
|
-
<td style="text-align:right;font-size:0.72rem;color:var(--text-muted);">${p.word_count ? p.word_count.toLocaleString() : '—'}</td>
|
|
2092
|
-
<td style="text-align:center;font-size:0.72rem;">${p.has_canonical ? '<span style="color:var(--color-success);">✓</span>' : '<span style="color:var(--text-muted);">—</span>'}</td>
|
|
2093
|
-
<td style="text-align:center;font-size:0.72rem;">${p.has_og_tags ? '<span style="color:var(--color-success);">✓</span>' : '<span style="color:var(--text-muted);">—</span>'}</td>
|
|
2094
|
-
</tr>`;
|
|
2095
|
-
}).join('')}
|
|
2096
|
-
</tbody>
|
|
2097
|
-
</table>
|
|
2098
|
-
</div>
|
|
2099
|
-
${targetPages.length >= 100 ? '<div style="font-size:0.65rem;color:var(--text-muted);margin-top:6px;">Showing first 100 pages. Run <code>seo-intel export ' + project + '</code> for full CSV export.</div>' : ''}
|
|
2100
|
-
</div>
|
|
2101
|
-
|
|
2102
|
-
<!-- INTERNAL LINK STRUCTURE -->
|
|
2103
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;">
|
|
2104
|
-
<div class="card">
|
|
2105
|
-
<h2><span class="icon"><i class="fa-solid fa-arrow-trend-up"></i></span> Most Linked Pages</h2>
|
|
2106
|
-
${topLinkedPages.length > 0 ? `
|
|
2107
|
-
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
2108
|
-
${topLinkedPages.map((p, i) => {
|
|
2109
|
-
const label = p.target_url.replace(/^https?:\/\/[^/]+/, '').slice(0, 45) || '/';
|
|
2110
|
-
const maxInbound = topLinkedPages[0].inbound || 1;
|
|
2111
|
-
const barPct = Math.round((p.inbound / maxInbound) * 100);
|
|
2112
|
-
return `<div style="font-size:0.7rem;">
|
|
2113
|
-
<div style="display:flex;justify-content:space-between;margin-bottom:2px;">
|
|
2114
|
-
<span style="color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80%;" title="${escapeHtml(p.target_url)}">${escapeHtml(label)}</span>
|
|
2115
|
-
<span style="color:var(--text-muted);flex-shrink:0;margin-left:8px;">${p.inbound}</span>
|
|
2116
|
-
</div>
|
|
2117
|
-
<div style="height:3px;background:var(--bg-elevated);border-radius:2px;overflow:hidden;">
|
|
2118
|
-
<div style="height:100%;width:${barPct}%;background:var(--accent-gold);border-radius:2px;opacity:0.6;"></div>
|
|
2119
|
-
</div>
|
|
2120
|
-
</div>`;
|
|
2121
|
-
}).join('')}
|
|
2122
|
-
</div>` : `
|
|
2123
|
-
<div style="font-size:0.72rem;color:var(--text-muted);">
|
|
2124
|
-
${internalLinks.topPages.slice(0, 8).map(p => `
|
|
2125
|
-
<div style="display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid var(--border-subtle);">
|
|
2126
|
-
<span style="color:var(--accent-gold);font-family:var(--font-display);min-width:20px;">${p.count}</span>
|
|
2127
|
-
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(p.label)}</span>
|
|
2128
|
-
</div>`).join('')}
|
|
2129
|
-
</div>`}
|
|
2130
|
-
</div>
|
|
2131
|
-
|
|
2132
|
-
<div class="card">
|
|
2133
|
-
<h2><span class="icon"><i class="fa-solid fa-triangle-exclamation"></i></span> Orphaned Pages</h2>
|
|
2134
|
-
${orphanPages.length === 0 ? `
|
|
2135
|
-
<div style="font-size:0.8rem;color:var(--color-success);padding:12px 0;">
|
|
2136
|
-
<i class="fa-solid fa-check-circle" style="margin-right:6px;"></i>No orphaned pages found.
|
|
2137
|
-
</div>` : `
|
|
2138
|
-
<div style="font-size:0.65rem;color:var(--color-warning);margin-bottom:8px;">
|
|
2139
|
-
<i class="fa-solid fa-warning" style="margin-right:4px;"></i>
|
|
2140
|
-
${orphanPages.length} indexed page${orphanPages.length !== 1 ? 's' : ''} with no inbound internal links
|
|
2141
|
-
</div>
|
|
2142
|
-
<div style="max-height:200px;overflow-y:auto;">
|
|
2143
|
-
${orphanPages.map(p => {
|
|
2144
|
-
const label = p.url.replace(/^https?:\/\/[^/]+/, '').slice(0, 55) || '/';
|
|
2145
|
-
return `<div style="font-size:0.68rem;color:var(--text-secondary);padding:3px 0;border-bottom:1px solid var(--border-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(p.url)}">${escapeHtml(label)}</div>`;
|
|
2146
|
-
}).join('')}
|
|
2147
|
-
</div>`}
|
|
2148
|
-
</div>
|
|
2149
|
-
</div>
|
|
2150
|
-
|
|
2151
|
-
<!-- SCHEMA COVERAGE + TECHNICAL SIGNALS -->
|
|
2152
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;">
|
|
2153
|
-
<div class="card">
|
|
2154
|
-
<h2><span class="icon"><i class="fa-solid fa-code"></i></span> Schema.org Coverage</h2>
|
|
2155
|
-
${schemaTypes.length > 0 ? `
|
|
2156
|
-
<div style="margin-bottom:14px;">
|
|
2157
|
-
<div style="font-size:0.65rem;color:var(--text-muted);margin-bottom:8px;">Found on your site:</div>
|
|
2158
|
-
${schemaTypes.map(s => {
|
|
2159
|
-
const isHighValue = HIGH_VALUE_SCHEMA.some(h => h.type === s.schema_type);
|
|
2160
|
-
return `<div style="display:flex;justify-content:space-between;font-size:0.7rem;padding:3px 0;">
|
|
2161
|
-
<span style="color:${isHighValue ? 'var(--color-success)' : 'var(--text-secondary)'};">
|
|
2162
|
-
${isHighValue ? '✅' : '⚪'} ${escapeHtml(s.schema_type)}
|
|
2163
|
-
</span>
|
|
2164
|
-
<span style="color:var(--text-muted);">${s.count} page${s.count !== 1 ? 's' : ''}</span>
|
|
2165
|
-
</div>`;
|
|
2166
|
-
}).join('')}
|
|
2167
|
-
</div>` : `
|
|
2168
|
-
<div style="font-size:0.72rem;color:var(--text-muted);margin-bottom:14px;">No structured data detected.</div>`}
|
|
2169
|
-
<div style="border-top:1px solid var(--border-subtle);padding-top:10px;">
|
|
2170
|
-
<div style="font-size:0.65rem;color:var(--text-muted);margin-bottom:8px;">High-value types to add:</div>
|
|
2171
|
-
${HIGH_VALUE_SCHEMA.map(h => {
|
|
2172
|
-
const present = foundSchemaTypes.has(h.type);
|
|
2173
|
-
return `<div style="display:flex;align-items:flex-start;gap:6px;font-size:0.68rem;padding:3px 0;">
|
|
2174
|
-
<span style="flex-shrink:0;">${present ? '✅' : '⚠️'}</span>
|
|
2175
|
-
<span>
|
|
2176
|
-
<span style="color:${present ? 'var(--color-success)' : 'var(--text-secondary)'};">${h.label}</span>
|
|
2177
|
-
${!present ? `<span style="color:var(--text-muted);display:block;font-size:0.62rem;">${h.benefit}</span>` : ''}
|
|
2178
|
-
</span>
|
|
2179
|
-
</div>`;
|
|
2180
|
-
}).join('')}
|
|
2181
|
-
</div>
|
|
2182
|
-
</div>
|
|
2183
|
-
|
|
2184
|
-
<div class="card">
|
|
2185
|
-
<h2><span class="icon"><i class="fa-solid fa-gear"></i></span> Technical Signals</h2>
|
|
2186
|
-
${techCoverage.total ? (() => {
|
|
2187
|
-
const pct = (n) => techCoverage.total ? Math.round(((n || 0) / techCoverage.total) * 100) : 0;
|
|
2188
|
-
const bar = (label, n) => {
|
|
2189
|
-
const p = pct(n);
|
|
2190
|
-
const color = p >= 90 ? 'var(--color-success)' : p >= 60 ? 'var(--color-warning)' : 'var(--color-danger)';
|
|
2191
|
-
return `<div style="margin-bottom:12px;"><div style="display:flex;justify-content:space-between;font-size:0.7rem;margin-bottom:3px;"><span style="color:var(--text-secondary);">${label}</span><span style="color:${color};">${p}% <span style="color:var(--text-muted);font-size:0.62rem;">(${n || 0}/${techCoverage.total})</span></span></div><div style="height:4px;background:var(--bg-elevated);border-radius:2px;overflow:hidden;"><div style="height:100%;width:${p}%;background:${color};border-radius:2px;"></div></div></div>`;
|
|
2192
|
-
};
|
|
2193
|
-
return bar('Canonical Tag', techCoverage.canonical) +
|
|
2194
|
-
bar('Open Graph Tags', techCoverage.og) +
|
|
2195
|
-
bar('Schema Markup', techCoverage.schema) +
|
|
2196
|
-
bar('Robots Meta', techCoverage.robots) +
|
|
2197
|
-
(avgLoad > 0 ? `<div style="font-size:0.65rem;color:var(--text-muted);margin-top:8px;">
|
|
2198
|
-
Avg load: <span style="color:${avgLoad > 3000 ? 'var(--color-danger)' : avgLoad > 1500 ? 'var(--color-warning)' : 'var(--color-success)'};">${avgLoad > 1000 ? (avgLoad/1000).toFixed(1) + 's' : avgLoad + 'ms'}</span>
|
|
2199
|
-
</div>` : '');
|
|
2200
|
-
})() : '<div style="font-size:0.72rem;color:var(--text-muted);">No technical data yet. Run a crawl first.</div>'}
|
|
2201
|
-
</div>
|
|
2202
|
-
</div>
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
<!-- Action exports available via CLI terminal panel -->
|
|
2206
|
-
|
|
2207
|
-
<!-- UPGRADE CTA -->
|
|
2208
|
-
<div style="text-align:center;padding:36px 24px;margin-bottom:16px;background:var(--bg-card);border:1px solid rgba(232,213,163,0.12);border-radius:var(--radius);">
|
|
2209
|
-
<i class="fa-solid fa-chart-column" style="font-size:1.2rem;color:var(--accent-gold);margin-bottom:10px;display:block;"></i>
|
|
2210
|
-
<h3 style="font-size:0.9rem;color:var(--text-primary);margin-bottom:6px;">See the Full Picture</h3>
|
|
2211
|
-
<p style="font-size:0.72rem;color:var(--text-muted);max-width:440px;margin:0 auto 14px;line-height:1.6;">
|
|
2212
|
-
This is your site's structure. To see how you compare to competitors —
|
|
2213
|
-
keyword gaps, content opportunities, pages you can outrank — upgrade to SEO Intel Solo.
|
|
2214
|
-
</p>
|
|
2215
|
-
<a href="https://ukkometa.fi/en/seo-intel/" target="_blank"
|
|
2216
|
-
style="display:inline-block;padding:10px 24px;background:var(--accent-gold);color:var(--text-dark);border-radius:var(--radius);font-size:0.8rem;font-weight:500;text-decoration:none;">
|
|
2217
|
-
Upgrade to Solo — €19.99/mo →
|
|
2218
|
-
</a>
|
|
2219
|
-
<div style="font-size:0.62rem;color:var(--text-muted);margin-top:8px;">
|
|
2220
|
-
€199/yr saves ~17%
|
|
2221
|
-
</div>
|
|
2222
|
-
<div style="font-size:0.62rem;color:var(--text-muted);margin-top:10px;padding-top:10px;border-top:1px solid var(--border-subtle);">
|
|
2223
|
-
Data crawled: ${lastCrawl} · Export raw CSV: <code style="background:var(--bg-elevated);padding:1px 5px;border-radius:3px;">seo-intel export ${project}</code>
|
|
2224
|
-
</div>
|
|
2225
|
-
</div>
|
|
2226
|
-
|
|
2227
|
-
</div>
|
|
2228
|
-
</div>`;
|
|
2229
|
-
|
|
2230
|
-
if (panelOnly) return panelHtml;
|
|
2231
|
-
|
|
2232
|
-
const freeScriptHtml = `<script>
|
|
2233
|
-
document.querySelectorAll('.header-bar').forEach(el => {
|
|
2234
|
-
el.style.maxWidth = 'var(--max-width)';
|
|
2235
|
-
el.style.margin = '0 auto';
|
|
2236
|
-
});
|
|
2237
|
-
</script>`;
|
|
2238
|
-
|
|
2239
|
-
return headHtml + '\n<body>\n' + panelHtml + '\n' + freeScriptHtml + '\n</body>\n</html>';
|
|
2240
|
-
}
|
|
2241
1892
|
|
|
2242
1893
|
// ── Panel HTML (project-specific body content) ──
|
|
2243
1894
|
const panelHtml = `
|
|
@@ -2357,16 +2008,16 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2357
2008
|
<span style="font-size:0.6rem;color:var(--text-muted);margin-right:4px;"><i class="fa-solid fa-play" style="margin-right:3px;"></i>Run:</span>
|
|
2358
2009
|
<button class="term-btn" data-cmd="crawl" data-project="${project}"><i class="fa-solid fa-spider"></i> Crawl</button>
|
|
2359
2010
|
<label class="term-stealth"><input type="checkbox" id="stealthToggle${suffix}"${extractionStatus.liveProgress?.stealth ? ' checked' : ''}><i class="fa-solid fa-user-ninja"></i></label>
|
|
2360
|
-
|
|
2361
|
-
<span style="width:1px;height:16px;background:var(--border-subtle);margin:0 2px;"></span>
|
|
2362
|
-
<button class="term-btn term-btn-intel" data-cmd="analyze" data-project="${project}"><i class="fa-solid fa-chart-column"></i> Analyze</button>
|
|
2363
|
-
<button class="term-btn term-btn-intel" data-cmd="brief" data-project="${project}"><i class="fa-solid fa-file-lines"></i> Brief</button>
|
|
2011
|
+
<button class="term-btn" data-cmd="extract" data-project="${project}"><i class="fa-solid fa-brain"></i> Extract</button>
|
|
2364
2012
|
<button class="term-btn term-btn-intel" data-cmd="keywords" data-project="${project}"><i class="fa-solid fa-key"></i> Keywords</button>
|
|
2365
|
-
<button class="term-btn term-btn-intel" data-cmd="templates" data-project="${project}"><i class="fa-solid fa-clone"></i> Templates</button
|
|
2013
|
+
<button class="term-btn term-btn-intel" data-cmd="templates" data-project="${project}"><i class="fa-solid fa-clone"></i> Templates</button>
|
|
2014
|
+
${showCompetitor ? `<span style="width:1px;height:16px;background:var(--border-subtle);margin:0 2px;"></span>
|
|
2015
|
+
<button class="term-btn term-btn-intel" data-cmd="analyze" data-project="${project}"><i class="fa-solid fa-chart-column"></i> Analyze</button>` : ''}
|
|
2016
|
+
${showHistory ? `<button class="term-btn term-btn-intel" data-cmd="brief" data-project="${project}"><i class="fa-solid fa-file-lines"></i> Brief</button>` : ''}
|
|
2366
2017
|
<button class="term-btn" data-cmd="status" data-project=""><i class="fa-solid fa-circle-info"></i> Status</button>
|
|
2367
2018
|
<button class="term-btn" data-cmd="guide" data-project="${project}"><i class="fa-solid fa-map"></i> Guide</button>
|
|
2368
2019
|
<button class="term-btn" data-cmd="setup" data-project="" style="margin-left:auto;border-color:rgba(232,213,163,0.25);"><i class="fa-solid fa-gear"></i> Setup</button>
|
|
2369
|
-
${!pro ? `<span style="font-size:0.55rem;color:var(--text-muted);margin-left:auto;"><i class="fa-solid fa-lock" style="color:var(--accent-gold);margin-right:3px;"></i><a href="https://ukkometa.fi/en/seo-intel/" target="_blank" style="color:var(--accent-gold);text-decoration:none;">Solo</a> for
|
|
2020
|
+
${!pro ? `<span style="font-size:0.55rem;color:var(--text-muted);margin-left:auto;"><i class="fa-solid fa-lock" style="color:var(--accent-gold);margin-right:3px;"></i><a href="https://ukkometa.fi/en/seo-intel/" target="_blank" style="color:var(--accent-gold);text-decoration:none;">Solo</a> for competitors, scheduling & history</span>` : ''}
|
|
2370
2021
|
</div>
|
|
2371
2022
|
<!-- Terminal output -->
|
|
2372
2023
|
<div id="termOutput${suffix}" style="padding:12px 16px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:0.68rem;line-height:1.7;color:var(--text-muted);max-height:400px;overflow-y:auto;min-height:60px;">
|
|
@@ -2386,11 +2037,10 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2386
2037
|
<i class="fa-solid fa-file-export"></i> Exports
|
|
2387
2038
|
<span style="margin-left:auto;font-size:.55rem;color:var(--text-muted);font-weight:400;letter-spacing:0;">→ reports/</span>
|
|
2388
2039
|
</div>
|
|
2389
|
-
${pro ? `
|
|
2390
2040
|
<div class="export-sidebar-btns">
|
|
2391
2041
|
<button class="export-btn" data-export-cmd="export-actions" data-export-project="${project}" data-export-scope="technical"><i class="fa-solid fa-wrench"></i> Technical Audit</button>
|
|
2392
|
-
|
|
2393
|
-
<button class="export-btn" data-export-cmd="suggest-usecases" data-export-project="${project}"><i class="fa-solid fa-lightbulb"></i> Suggest What to Build</button
|
|
2042
|
+
${showCompetitor ? `<button class="export-btn" data-export-cmd="export-actions" data-export-project="${project}" data-export-scope="competitive"><i class="fa-solid fa-users"></i> Competitive Gaps</button>
|
|
2043
|
+
<button class="export-btn" data-export-cmd="suggest-usecases" data-export-project="${project}"><i class="fa-solid fa-lightbulb"></i> Suggest What to Build</button>` : ''}
|
|
2394
2044
|
</div>
|
|
2395
2045
|
<div class="export-sidebar-header" style="margin-top:12px;">
|
|
2396
2046
|
<i class="fa-solid fa-pen-fancy"></i> Create
|
|
@@ -2436,16 +2086,8 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2436
2086
|
</div>
|
|
2437
2087
|
<button class="export-btn download-all-btn" data-project="${project}" style="font-size:0.58rem;opacity:0.6;"><i class="fa-solid fa-file-zipper"></i> Raw Full Export (ZIP)</button>
|
|
2438
2088
|
</div>
|
|
2439
|
-
` : `
|
|
2440
|
-
<div style="padding:20px 14px;text-align:center;">
|
|
2441
|
-
<i class="fa-solid fa-lock" style="font-size:1rem;color:var(--accent-gold);margin-bottom:8px;display:block;"></i>
|
|
2442
|
-
<p style="font-size:0.68rem;color:var(--text-muted);line-height:1.5;margin-bottom:12px;">Agentic exports turn your crawl data into implementation briefs.</p>
|
|
2443
|
-
<a href="https://ukkometa.fi/en/seo-intel/" target="_blank" style="display:inline-block;padding:6px 14px;background:var(--accent-gold);color:var(--text-dark);border-radius:var(--radius);font-size:0.68rem;font-weight:500;text-decoration:none;">Unlock with Solo</a>
|
|
2444
|
-
</div>
|
|
2445
|
-
`}
|
|
2446
2089
|
</div>
|
|
2447
2090
|
</div>
|
|
2448
|
-
${pro ? `
|
|
2449
2091
|
<div class="viewer-row" style="max-width:var(--max-width);margin:0 auto;">
|
|
2450
2092
|
<div style="position:relative;background:#0e0e0e;border:1px solid var(--border-card);border-radius:0 0 var(--radius) var(--radius);border-top:none;">
|
|
2451
2093
|
<div id="exportSaveStatus${suffix}" style="display:none;padding:4px 10px;font-size:.6rem;color:var(--color-success);background:rgba(80,200,120,0.06);border-bottom:1px solid rgba(80,200,120,0.15);font-family:'SF Mono',monospace;">
|
|
@@ -2460,7 +2102,6 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2460
2102
|
</div>
|
|
2461
2103
|
</div>
|
|
2462
2104
|
</div>
|
|
2463
|
-
` : ''}
|
|
2464
2105
|
|
|
2465
2106
|
<script>
|
|
2466
2107
|
(function() {
|
|
@@ -3158,8 +2799,8 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3158
2799
|
</div>`;
|
|
3159
2800
|
})() : ''}
|
|
3160
2801
|
|
|
3161
|
-
<!-- ═══ GSC INSIGHTS ═══ -->
|
|
3162
|
-
${
|
|
2802
|
+
<!-- ═══ GSC INSIGHTS ═══ (free — your own Search Console) -->
|
|
2803
|
+
${gscInsights ? (() => {
|
|
3163
2804
|
const blocks = gscInsights.map(insight => {
|
|
3164
2805
|
const itemsHtml = insight.items.length ? `
|
|
3165
2806
|
<div class="gsc-insight-items">
|
|
@@ -3196,7 +2837,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3196
2837
|
</div>
|
|
3197
2838
|
|
|
3198
2839
|
<!-- ═══ KEYWORD VENN BATTLEFIELD ═══ -->
|
|
3199
|
-
${
|
|
2840
|
+
${showCompetitor && keywordVenn.hasData ? `
|
|
3200
2841
|
<div class="card" id="keyword-venn">
|
|
3201
2842
|
<h2><span class="icon"><i class="fa-solid fa-crosshairs"></i></span> Keyword Venn Battlefield</h2>
|
|
3202
2843
|
<canvas id="vennCanvas${suffix}" width="540" height="400"></canvas>
|
|
@@ -3212,7 +2853,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3212
2853
|
</div>
|
|
3213
2854
|
|
|
3214
2855
|
<!-- ═══ COMPETITIVE GRAVITY MAP ═══ -->
|
|
3215
|
-
${
|
|
2856
|
+
${showCompetitor ? `
|
|
3216
2857
|
<div class="card" id="gravity-map">
|
|
3217
2858
|
<h2><span class="icon"><i class="fa-solid fa-diagram-project"></i></span> Competitive Gravity Map</h2>
|
|
3218
2859
|
<canvas id="gravityCanvas${suffix}" width="540" height="440"></canvas>
|
|
@@ -3249,7 +2890,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3249
2890
|
</div>
|
|
3250
2891
|
|
|
3251
2892
|
<!-- ═══ TERRITORY CONTROL MAP ═══ -->
|
|
3252
|
-
${
|
|
2893
|
+
${showCompetitor ? `
|
|
3253
2894
|
<div class="card full-width" id="territory-map">
|
|
3254
2895
|
<h2><span class="icon"><i class="fa-solid fa-chess-rook"></i></span> Territory Control Map</h2>
|
|
3255
2896
|
<canvas id="treemapCanvas${suffix}" width="1100" height="400"></canvas>
|
|
@@ -3269,7 +2910,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3269
2910
|
` : ''}
|
|
3270
2911
|
|
|
3271
2912
|
<!-- ═══ ENTITY TOPIC MAP ═══ -->
|
|
3272
|
-
${
|
|
2913
|
+
${showCompetitor && entityTopicMap.hasData ? `
|
|
3273
2914
|
<div class="card full-width" id="entity-map">
|
|
3274
2915
|
${cardExportHtml('insights', project)}
|
|
3275
2916
|
<h2><span class="icon"><i class="fa-solid fa-map"></i></span> Entity Topic Map</h2>
|
|
@@ -3294,7 +2935,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3294
2935
|
` : ''}
|
|
3295
2936
|
|
|
3296
2937
|
<!-- ═══ KEYWORD BATTLEGROUND ═══ -->
|
|
3297
|
-
${
|
|
2938
|
+
${showCompetitor ? `
|
|
3298
2939
|
<div class="card full-width" id="keyword-heatmap">
|
|
3299
2940
|
${cardExportHtml('keywords', project)}
|
|
3300
2941
|
<h2><span class="icon"><i class="fa-solid fa-shield-halved"></i></span> Keyword Battleground</h2>
|
|
@@ -3408,7 +3049,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3408
3049
|
})() : ''}
|
|
3409
3050
|
|
|
3410
3051
|
<!-- ═══ TECHNICAL SEO GAPS ═══ -->
|
|
3411
|
-
${
|
|
3052
|
+
${showCompetitor && latestAnalysis?.technical_gaps?.length ? `
|
|
3412
3053
|
<div class="card full-width" id="technical-gaps">
|
|
3413
3054
|
${cardExportHtml('technical', project)}
|
|
3414
3055
|
<h2><span class="icon"><i class="fa-solid fa-wrench"></i></span> Technical SEO Gaps</h2>
|
|
@@ -3429,7 +3070,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3429
3070
|
</div>
|
|
3430
3071
|
` : ''}
|
|
3431
3072
|
|
|
3432
|
-
${
|
|
3073
|
+
${showCompetitor ? `
|
|
3433
3074
|
<div class="section-divider">
|
|
3434
3075
|
<div class="section-divider-line right"></div>
|
|
3435
3076
|
<span class="section-divider-label"><i class="fa-solid fa-bolt"></i> Strategy & Actions</span>
|
|
@@ -3437,7 +3078,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3437
3078
|
</div>` : ''}
|
|
3438
3079
|
|
|
3439
3080
|
<!-- ═══ QUICK WINS ═══ -->
|
|
3440
|
-
${
|
|
3081
|
+
${showCompetitor && latestAnalysis?.quick_wins?.length ? `
|
|
3441
3082
|
<div class="card" id="quick-wins">
|
|
3442
3083
|
${cardExportHtml('insights', project)}
|
|
3443
3084
|
<h2><span class="icon"><i class="fa-solid fa-bolt"></i></span> Quick Wins</h2>
|
|
@@ -3460,7 +3101,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3460
3101
|
` : ''}
|
|
3461
3102
|
|
|
3462
3103
|
<!-- ═══ NEW PAGES TO CREATE ═══ -->
|
|
3463
|
-
${
|
|
3104
|
+
${showCompetitor && latestAnalysis?.new_pages?.length ? `
|
|
3464
3105
|
<div class="card" id="new-pages">
|
|
3465
3106
|
${cardExportHtml('pages', project)}
|
|
3466
3107
|
<h2><span class="icon"><i class="fa-solid fa-file-circle-plus"></i></span> New Pages to Create</h2>
|
|
@@ -3489,7 +3130,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3489
3130
|
` : ''}
|
|
3490
3131
|
|
|
3491
3132
|
<!-- ═══ POSITIONING STRATEGY ═══ -->
|
|
3492
|
-
${
|
|
3133
|
+
${showCompetitor && latestAnalysis?.positioning ? `
|
|
3493
3134
|
<div class="card full-width" id="positioning">
|
|
3494
3135
|
<h2><span class="icon"><i class="fa-solid fa-crosshairs"></i></span> Positioning Strategy</h2>
|
|
3495
3136
|
<div class="positioning-grid">
|
|
@@ -3513,7 +3154,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3513
3154
|
` : ''}
|
|
3514
3155
|
|
|
3515
3156
|
<!-- ═══ CONTENT GAPS ═══ -->
|
|
3516
|
-
${
|
|
3157
|
+
${showCompetitor && latestAnalysis?.content_gaps?.length ? `
|
|
3517
3158
|
<div class="card full-width" id="content-gaps">
|
|
3518
3159
|
${cardExportHtml('insights', project)}
|
|
3519
3160
|
<h2><span class="icon"><i class="fa-solid fa-magnifying-glass-minus"></i></span> Content Gaps</h2>
|
|
@@ -3535,7 +3176,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3535
3176
|
` : ''}
|
|
3536
3177
|
|
|
3537
3178
|
<!-- ═══ TOPIC CLUSTER GAPS ═══ -->
|
|
3538
|
-
${
|
|
3179
|
+
${showCompetitor && topicClusters ? `
|
|
3539
3180
|
<div class="card full-width" id="topic-cluster-gaps">
|
|
3540
3181
|
<h2><span class="icon"><i class="fa-solid fa-diagram-project"></i></span> Topic Cluster Coverage</h2>
|
|
3541
3182
|
<p class="attack-desc">Semantic topic clusters — pages grouped by theme. Red = gap vs competitors. Green = target is competitive.</p>
|
|
@@ -3564,7 +3205,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3564
3205
|
` : ''}
|
|
3565
3206
|
|
|
3566
3207
|
<!-- ═══ SHALLOW CHAMPIONS ═══ -->
|
|
3567
|
-
${
|
|
3208
|
+
${showCompetitor ? `
|
|
3568
3209
|
<div class="card" id="shallow-champions">
|
|
3569
3210
|
<h2><span class="icon"><i class="fa-solid fa-trophy"></i></span> Shallow Champions <span class="attack-count">${shallowChampions.total}</span></h2>
|
|
3570
3211
|
<p class="attack-desc">Competitor pages at depth 1-2 with under 700 words — validated topics, thin content. Out-invest them.</p>
|
|
@@ -3701,7 +3342,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3701
3342
|
` : ''}
|
|
3702
3343
|
|
|
3703
3344
|
<!-- ═══ CTA LANDSCAPE ═══ -->
|
|
3704
|
-
${
|
|
3345
|
+
${showCompetitor && ctaLandscape.hasData ? `
|
|
3705
3346
|
<div class="card" id="cta-landscape">
|
|
3706
3347
|
<h2><span class="icon"><i class="fa-solid fa-bullhorn"></i></span> CTA Landscape</h2>
|
|
3707
3348
|
<div class="attack-table-wrap">
|
|
@@ -3748,8 +3389,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3748
3389
|
</div>
|
|
3749
3390
|
` : ''}
|
|
3750
3391
|
|
|
3751
|
-
<!-- ═══ TOP KEYWORDS ═══ -->
|
|
3752
|
-
${pro ? `
|
|
3392
|
+
<!-- ═══ TOP KEYWORDS ═══ (free — your own site) -->
|
|
3753
3393
|
<div class="card" id="top-keywords">
|
|
3754
3394
|
${cardExportHtml('keywords', project)}
|
|
3755
3395
|
<h2><span class="icon"><i class="fa-solid fa-key"></i></span> Top Keywords (${targetDomain})</h2>
|
|
@@ -3783,7 +3423,6 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3783
3423
|
</div>
|
|
3784
3424
|
` : '<div class="empty-state">No keyword data available</div>'}
|
|
3785
3425
|
</div>
|
|
3786
|
-
` : ''}
|
|
3787
3426
|
|
|
3788
3427
|
<!-- ═══ INTERNAL LINK ANALYSIS ═══ -->
|
|
3789
3428
|
<div class="card" id="internal-links">
|
|
@@ -3806,10 +3445,10 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3806
3445
|
|
|
3807
3446
|
${!pro ? `
|
|
3808
3447
|
<div class="card full-width" style="text-align:center; padding:40px 24px;">
|
|
3809
|
-
<i class="fa-solid fa-
|
|
3810
|
-
<h3 style="font-size:0.9rem; color:var(--text-primary); margin-bottom:8px;">
|
|
3811
|
-
<p style="font-size:0.75rem; color:var(--text-muted); max-width:
|
|
3812
|
-
|
|
3448
|
+
<i class="fa-solid fa-radar" style="font-size:1.5rem; color:var(--accent-gold); margin-bottom:12px;"></i>
|
|
3449
|
+
<h3 style="font-size:0.9rem; color:var(--text-primary); margin-bottom:8px;">Your own site is fully analyzed — free. Now watch the competition.</h3>
|
|
3450
|
+
<p style="font-size:0.75rem; color:var(--text-muted); max-width:460px; margin:0 auto 16px;">
|
|
3451
|
+
Solo adds what an agent can't do for itself: <strong>competitor synthesis</strong> (gaps, keyword battleground, positioning), <strong>scheduled crawls</strong> that run themselves, and <strong>history & trends</strong> that show what changed over time.
|
|
3813
3452
|
</p>
|
|
3814
3453
|
<a href="https://ukkometa.fi/en/seo-intel/" target="_blank"
|
|
3815
3454
|
style="display:inline-block; padding:8px 20px; background:var(--accent-gold); color:var(--text-dark); border-radius:var(--radius); font-size:0.78rem; font-weight:500; text-decoration:none;">
|
|
@@ -3818,18 +3457,17 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3818
3457
|
</div>
|
|
3819
3458
|
` : ''}
|
|
3820
3459
|
|
|
3821
|
-
${pro ? `
|
|
3822
3460
|
<div class="section-divider">
|
|
3823
3461
|
<div class="section-divider-line right"></div>
|
|
3824
3462
|
<span class="section-divider-label"><i class="fa-solid fa-flask"></i> Research</span>
|
|
3825
3463
|
<div class="section-divider-line"></div>
|
|
3826
|
-
</div
|
|
3464
|
+
</div>
|
|
3827
3465
|
|
|
3828
|
-
<!-- ═══ AEO / AI CITABILITY AUDIT ═══ -->
|
|
3829
|
-
${
|
|
3466
|
+
<!-- ═══ AEO / AI CITABILITY AUDIT ═══ (free — your own site) -->
|
|
3467
|
+
${citabilityData?.length ? buildAeoCard(citabilityData, escapeHtml, project) : ''}
|
|
3830
3468
|
|
|
3831
3469
|
<!-- ═══ LONG-TAIL OPPORTUNITIES ═══ -->
|
|
3832
|
-
${
|
|
3470
|
+
${showCompetitor && latestAnalysis?.long_tails?.length ? `
|
|
3833
3471
|
<div class="card full-width" id="long-tails">
|
|
3834
3472
|
<h2><span class="icon"><i class="fa-solid fa-binoculars"></i></span> Long-tail Opportunities</h2>
|
|
3835
3473
|
<div class="analysis-table-wrap">
|
|
@@ -3856,8 +3494,8 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
3856
3494
|
</div>
|
|
3857
3495
|
` : ''}
|
|
3858
3496
|
|
|
3859
|
-
<!-- ═══ KEYWORD INVENTOR ═══ -->
|
|
3860
|
-
${
|
|
3497
|
+
<!-- ═══ KEYWORD INVENTOR ═══ (free — your own site) -->
|
|
3498
|
+
${keywordsReport ? (() => {
|
|
3861
3499
|
const allClusters = keywordsReport.keyword_clusters || [];
|
|
3862
3500
|
const allKws = allClusters.flatMap(c => (c.keywords || []).map(k => ({ ...k, cluster: c.topic })));
|
|
3863
3501
|
const totalPhrases = allKws.length;
|