v6: Cloud fallback, quality gate, format diversity, real fact-check, scroll depth, email capture, dual-model research, vision-verified images, network footer
This commit is contained in:
@@ -234,6 +234,14 @@ def init_db():
|
||||
CREATE INDEX IF NOT EXISTS idx_pageviews_created ON pageviews(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_published ON articles(published_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_slug ON articles(slug);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subscribers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
vertical TEXT DEFAULT '',
|
||||
confirmed INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
@@ -601,6 +609,22 @@ def api_learn():
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/subscribe", methods=["POST"])
|
||||
def api_subscribe():
|
||||
"""Email newsletter signup."""
|
||||
email = (request.json or {}).get("email", "").strip().lower()
|
||||
if not email or "@" not in email:
|
||||
return jsonify({"error": "invalid email"}), 400
|
||||
db = get_db()
|
||||
try:
|
||||
db.execute("INSERT OR IGNORE INTO subscribers (email, vertical) VALUES (?, ?)",
|
||||
(email, VERTICAL))
|
||||
db.commit()
|
||||
return jsonify({"status": "subscribed"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
db = get_db()
|
||||
@@ -1024,6 +1048,13 @@ ARTICLE_TEMPLATE = """<!DOCTYPE html>
|
||||
.network-link span{display:block;color:var(--text-muted);font-size:0.7rem;font-weight:400;margin-top:0.1rem}
|
||||
.footer-bottom{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;color:var(--text-muted);font-size:0.8rem}
|
||||
.footer-bottom a{color:var(--text-muted);text-decoration:none}
|
||||
|
||||
/* Newsletter */
|
||||
.newsletter-box{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.25rem;margin-top:2rem;text-align:center}
|
||||
.newsletter-box h4{font-family:var(--font-heading);font-size:1rem;margin-bottom:0.75rem}
|
||||
.subscribe-form{display:flex;gap:0.5rem;max-width:400px;margin:0 auto}
|
||||
.subscribe-form input{flex:1;padding:0.6rem 0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.9rem}
|
||||
.subscribe-form button{background:var(--gradient);color:white;border:none;padding:0.6rem 1.25rem;border-radius:6px;cursor:pointer;font-weight:600;font-size:0.9rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1077,11 +1108,21 @@ ARTICLE_TEMPLATE = """<!DOCTYPE html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="newsletter-box">
|
||||
<h4>📬 Get new articles by email</h4>
|
||||
<form class="subscribe-form" onsubmit="subscribe(event)">
|
||||
<input type="email" id="sub-email" placeholder="your@email.com" required>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
<div id="sub-msg" style="margin-top:0.5rem;font-size:0.8rem;display:none"></div>
|
||||
<p style="font-size:0.7rem;color:var(--text-muted);margin-top:0.5rem">No spam. Just new articles from {{ name }}.</p>
|
||||
</div>
|
||||
|
||||
{% if related %}
|
||||
<section class="related">
|
||||
<h2>Continue Reading</h2>
|
||||
<div class="related-grid">
|
||||
{% for r in related %}
|
||||
{% for r in related[:3] %}
|
||||
<a href="/articles/{{ r.slug }}" class="related-card">
|
||||
<h4>{{ r.title[:60] }}</h4>
|
||||
<div class="meta">{{ r.reading_time }} min · {{ r.published_at[:10] }}</div>
|
||||
@@ -1139,6 +1180,36 @@ ARTICLE_TEMPLATE = """<!DOCTYPE html>
|
||||
toc.appendChild(li);
|
||||
});
|
||||
})();
|
||||
|
||||
// Scroll depth tracking
|
||||
(function(){
|
||||
var fired = {25:false,50:false,75:false,100:false};
|
||||
window.addEventListener('scroll', function(){
|
||||
var pct = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
|
||||
[25,50,75,100].forEach(function(threshold){
|
||||
if(pct >= threshold && !fired[threshold]){
|
||||
fired[threshold] = true;
|
||||
new Image().src = '/a/ping?p=/articles/{{ article.slug }}&d=' + threshold;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Time on page ping every 30s
|
||||
setInterval(function(){
|
||||
new Image().src = '/a/ping?p=/articles/{{ article.slug }}&t=1';
|
||||
}, 30000);
|
||||
})();
|
||||
|
||||
function subscribe(e){
|
||||
e.preventDefault();
|
||||
var email = document.getElementById('sub-email').value;
|
||||
var msg = document.getElementById('sub-msg');
|
||||
fetch('/api/subscribe', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email})})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
msg.style.display = 'block';
|
||||
msg.textContent = d.status === 'subscribed' ? '✅ Subscribed! Welcome.' : '❌ ' + (d.error || 'Error');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<img src="/a/ping?p=/articles/{{ article.slug }}" alt="" width="1" height="1" style="display:none">
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user