diff --git a/data/insights.json b/data/insights.json new file mode 100644 index 0000000..3210654 --- /dev/null +++ b/data/insights.json @@ -0,0 +1,18 @@ +{ + "week": "—", + "updated": "—", + "visitors": 0, + "visitors_change": "", + "pageviews": 0, + "pageviews_change": "", + "bounce_rate": "—", + "bounce_rate_change": "", + "visit_duration": "—", + "visit_duration_change": "", + "top_pages": [], + "top_referrers": [], + "events": [], + "what_worked": [], + "what_didnt": [], + "recommendations": [] +} \ No newline at end of file diff --git a/index.html b/index.html index 84ee739..cd7a8c3 100644 --- a/index.html +++ b/index.html @@ -1692,6 +1692,7 @@ })(); function slideSpeedrun(dir) { + plausible('Speedrun Nav', { props: { direction: dir > 0 ? 'next' : 'prev' } }); const slider = document.getElementById('speedrunSlider'); if (!slider) return; const slideWidth = slider.querySelector('.video-slide').offsetWidth + 20; @@ -2187,12 +2188,15 @@
diff --git a/launch-dashboard.html b/launch-dashboard.html index 7f04152..8c28cf9 100644 --- a/launch-dashboard.html +++ b/launch-dashboard.html @@ -485,6 +485,91 @@ border: 1px solid var(--border); } .btn-ghost:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } + + /* ── Insights Section ───────────────────────────── */ + .insight-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; + } + .insight-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-card); + padding: 20px; + } + .insight-card h3 { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--accent); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 6px; + } + .insight-stat { + font-size: 32px; + font-weight: 700; + color: var(--text); + line-height: 1.1; + } + .insight-stat-label { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + } + .insight-list { + list-style: none; + padding: 0; + margin: 0; + } + .insight-list li { + padding: 6px 0; + font-size: 13px; + display: flex; + align-items: flex-start; + gap: 8px; + border-bottom: 1px solid rgba(10,48,96,0.3); + } + .insight-list li:last-child { border-bottom: none; } + .insight-list .val { color: var(--accent); font-weight: 600; margin-left: auto; flex-shrink: 0; } + .insight-rec { + background: var(--bg-card); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: var(--radius-card); + padding: 16px 20px; + margin-top: 12px; + font-size: 13px; + line-height: 1.6; + } + .insight-rec h4 { + color: var(--accent); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 6px; + } + .insight-empty { + color: var(--text-muted); + font-size: 13px; + text-align: center; + padding: 40px 0; + } + .insight-whatworked { color: var(--success); } + .insight-whatdidnt { color: var(--danger); } + .insight-neut { color: var(--warning); } + .insight-meta { + font-size: 11px; + color: var(--text-muted); + margin-top: 12px; + text-align: right; + } + @media (max-width: 700px) { + .insight-grid { grid-template-columns: 1fr; } + } @@ -531,6 +616,20 @@ + +
+
+

📈 Weekly SEO & Analytics Insights

+ +
+
+
+
Waiting for first analytics report. Data populates weekly from Plausible.
+
+
+
+
+
@@ -924,6 +1023,78 @@ return NEEDS; } + /* ── Render Weekly Insights ─────────────────── */ + function renderInsights(insights) { + const container = document.getElementById("insights-content"); + const meta = document.getElementById("insights-meta"); + if (!insights || !insights.week) { + container.innerHTML = '
Waiting for first analytics report. Data populates weekly from Plausible.
'; + meta.textContent = ""; + return; + } + + const whatWorked = (insights.what_worked || []).map(function (s) { + return '
  • ' + s + '
  • '; + }).join(""); + const whatDidnt = (insights.what_didnt || []).map(function (s) { + return '
  • ' + s + '
  • '; + }).join(""); + const recs = (insights.recommendations || []).map(function (s) { + return '
  • → ' + s + '
  • '; + }).join(""); + + let topPages = "", topRefs = ""; + if (insights.top_pages && insights.top_pages.length) { + topPages = insights.top_pages.map(function (p) { + return '
  • ' + p.page + ' ' + p.visits + '
  • '; + }).join(""); + } + if (insights.top_referrers && insights.top_referrers.length) { + topRefs = insights.top_referrers.map(function (r) { + return '
  • ' + r.source + ' ' + r.visits + '
  • '; + }).join(""); + } + + container.innerHTML = '' + + '
    ' + + '
    ' + + '

    👥 Visitors

    ' + + '
    ' + (insights.visitors || 0) + '
    ' + + '
    ' + (insights.visitors_change || '') + '
    ' + + '
    ' + + '
    ' + + '

    📄 Pageviews

    ' + + '
    ' + (insights.pageviews || 0) + '
    ' + + '
    ' + (insights.pageviews_change || '') + '
    ' + + '
    ' + + '
    ' + + '

    🔄 Bounce Rate

    ' + + '
    ' + (insights.bounce_rate || '—') + '
    ' + + '
    ' + (insights.bounce_rate_change || '') + '
    ' + + '
    ' + + '
    ' + + '

    ⏱ Visit Duration

    ' + + '
    ' + (insights.visit_duration || '—') + '
    ' + + '
    ' + (insights.visit_duration_change || '') + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    📑 Top Pages

    ' + + (topPages ? '
      ' + topPages + '
    ' : '
    No data yet
    ') + + '
    ' + + '
    ' + + '

    🔗 Top Referrers

    ' + + (topRefs ? '
      ' + topRefs + '
    ' : '
    No data yet
    ') + + '
    ' + + '
    ' + + (whatWorked ? '

    ✅ What Worked

      ' + whatWorked + '
    ' : '') + + (whatDidnt ? '

    ❌ What Didn\'t

      ' + whatDidnt + '
    ' : '') + + (recs ? '

    💡 Recommendations

      ' + recs + '
    ' : ''); + + meta.textContent = "Updated: " + (insights.updated || "—") + " — Week: " + insights.week; + } + /* ── Toggle sections ─────────────────────────── */ function toggleSection(id, header) { const body = document.getElementById(id); @@ -964,6 +1135,16 @@ renderKPIs(KPIS); renderActions(ACTIONS); } + + // Also load insights + try { + const ir = await fetch("data/insights.json?_=" + Date.now()); + if (ir.ok) { + renderInsights(await ir.json()); + } + } catch(e) { + // No insights yet — leave placeholder + } } /* ── Init ────────────────────────────────────── */ diff --git a/scripts/check-plausible-email.py b/scripts/check-plausible-email.py new file mode 100755 index 0000000..5cb6ee0 --- /dev/null +++ b/scripts/check-plausible-email.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Check for Plausible analytics email and output raw content for cron analysis.""" + +import json +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +HERE = Path(__file__).resolve().parent.parent +INSIGHTS_FILE = HERE / "data" / "insights.json" + + +def run_cmd(*args, timeout=30): + result = subprocess.run(args, capture_output=True, text=True, timeout=timeout) + if result.returncode != 0: + return None, result.stderr + return result.stdout, None + + +def find_plausible_email(): + """Find the latest Plausible-related email in inbox.""" + out, err = run_cmd("himalaya", "envelope", "list", "--page", "1", "--page-size", "30") + if not out: + return None, None + + best_id, best_subject = None, "" + current_id, current_subject = None, "" + for line in out.splitlines(): + # himalaya format: "ID | DATE | FROM | SUBJECT" + if "|" not in line: + continue + parts = [p.strip() for p in line.split("|")] + if len(parts) < 4: + continue + try: + current_id = int(parts[0]) + except ValueError: + continue + current_subject = parts[-1] if len(parts) >= 4 else "" + + subj_lower = current_subject.lower() + score = 0 + if "plausible" in subj_lower: + score += 10 + if "weekly" in subj_lower and ("report" in subj_lower or "analytics" in subj_lower or "stats" in subj_lower): + score += 5 + if "analytics" in subj_lower and "summary" in subj_lower: + score += 5 + if "site" in subj_lower and ("stats" in subj_lower or "report" in subj_lower): + score += 3 + + if score > 0 and score >= (10 if best_id is None else 0): + best_id = current_id + best_subject = current_subject + + return best_id, best_subject + + +def main(): + email_id, subject = find_plausible_email() + + if email_id is None: + print("No Plausible email found in inbox.", file=sys.stderr) + # Still update last_check timestamp + insights = {} + if INSIGHTS_FILE.exists(): + with open(INSIGHTS_FILE) as f: + insights = json.load(f) + insights["last_check"] = datetime.now().strftime("%Y-%m-%d %H:%M") + with open(INSIGHTS_FILE, "w") as f: + json.dump(insights, f, indent=2) + sys.exit(0) + + content, err = run_cmd("himalaya", "message", "read", str(email_id)) + if not content: + print(f"Failed to read email {email_id}: {err}", file=sys.stderr) + sys.exit(0) + + print(f"=== Plausible Email Found ===") + print(f"Subject: {subject}") + print(f"Email ID: {email_id}") + print(f"=== Full Content ===") + print(content[:6000]) + + +if __name__ == "__main__": + main() \ No newline at end of file