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.
@@ -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 ${pro ? 'Dashboard' : 'Preview'} — ${project.toUpperCase()}</title>
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
- ${pro ? `<button class="term-btn" data-cmd="extract" data-project="${project}"><i class="fa-solid fa-brain"></i> Extract</button>
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 extract, analyze, exports</span>` : ''}
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
- <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>
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
- ${pro && gscInsights ? (() => {
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
- ${pro && keywordVenn.hasData ? `
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
- ${pro ? `
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
- ${pro ? `
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
- ${pro && entityTopicMap.hasData ? `
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
- ${pro ? `
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
- ${pro && latestAnalysis?.technical_gaps?.length ? `
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
- ${pro ? `
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
- ${pro && latestAnalysis?.quick_wins?.length ? `
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
- ${pro && latestAnalysis?.new_pages?.length ? `
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
- ${pro && latestAnalysis?.positioning ? `
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
- ${pro && latestAnalysis?.content_gaps?.length ? `
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
- ${pro && topicClusters ? `
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
- ${pro ? `
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
- ${pro && ctaLandscape.hasData ? `
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-lock" style="font-size:1.5rem; color:var(--accent-gold); margin-bottom:12px;"></i>
3810
- <h3 style="font-size:0.9rem; color:var(--text-primary); margin-bottom:8px;">Unlock AI Analysis, Keywords & Strategy</h3>
3811
- <p style="font-size:0.75rem; color:var(--text-muted); max-width:400px; margin:0 auto 16px;">
3812
- Upgrade to Solo for keyword battleground, competitive gaps, AI-powered quick wins, content audits, and 15+ advanced visualizations.
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 &amp; 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
- ${pro && citabilityData?.length ? buildAeoCard(citabilityData, escapeHtml, project) : ''}
3466
+ <!-- ═══ AEO / AI CITABILITY AUDIT ═══ (free — your own site) -->
3467
+ ${citabilityData?.length ? buildAeoCard(citabilityData, escapeHtml, project) : ''}
3830
3468
 
3831
3469
  <!-- ═══ LONG-TAIL OPPORTUNITIES ═══ -->
3832
- ${pro && latestAnalysis?.long_tails?.length ? `
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
- ${pro && keywordsReport ? (() => {
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;