v2: Hero images, tag pages, 16 seed articles, FAL-generated visuals, nginx asset serving
This commit is contained in:
@@ -408,6 +408,20 @@ def sitemap():
|
|||||||
return Response(build_sitemap_xml(), mimetype="application/xml")
|
return Response(build_sitemap_xml(), mimetype="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/tag/<tag>")
|
||||||
|
def tag_page(tag):
|
||||||
|
"""Aggregate all articles with a given tag."""
|
||||||
|
db = get_db()
|
||||||
|
rows = db.execute("""
|
||||||
|
SELECT * FROM articles WHERE status='published' AND keywords LIKE ?
|
||||||
|
ORDER BY published_at DESC LIMIT 30
|
||||||
|
""", (f"%{tag}%",)).fetchall()
|
||||||
|
articles = [dict(r) for r in rows]
|
||||||
|
record_view(f"/tag/{tag}")
|
||||||
|
return render_template_string(TAG_TEMPLATE, **IDENTITY,
|
||||||
|
tag=tag, articles=articles, domain=DOMAIN, vertical=VERTICAL)
|
||||||
|
|
||||||
|
|
||||||
# ─── Analytics Collector ──────────────────────────────────────────
|
# ─── Analytics Collector ──────────────────────────────────────────
|
||||||
@app.route("/a/ping")
|
@app.route("/a/ping")
|
||||||
def analytics_ping():
|
def analytics_ping():
|
||||||
@@ -680,7 +694,8 @@ HOME_TEMPLATE = """<!DOCTYPE html>
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero" style="background: linear-gradient(180deg, transparent 0%, var(--bg) 85%), url('/assets/hero.png') center/cover no-repeat; min-height: 420px; display: flex; flex-direction: column; justify-content: center; position: relative;">
|
||||||
|
<div style="padding: 2rem 0;">
|
||||||
<h1>{{ tagline }}</h1>
|
<h1>{{ tagline }}</h1>
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
<div class="hero-stats">
|
<div class="hero-stats">
|
||||||
@@ -688,6 +703,7 @@ HOME_TEMPLATE = """<!DOCTYPE html>
|
|||||||
<span><strong>{{ total_views }}</strong> readers</span>
|
<span><strong>{{ total_views }}</strong> readers</span>
|
||||||
<span>Updated <strong>daily</strong></span>
|
<span>Updated <strong>daily</strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -1010,6 +1026,51 @@ SEARCH_TEMPLATE = """<!DOCTYPE html>
|
|||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
TAG_TEMPLATE = """<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ tag }} — {{ name }}</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:{{ bg }};--card-bg:{{ card_bg }};--text:#e2e8f0;--text-muted:#94a3b8;--primary:{{ primary }};--accent:{{ accent }};--gradient:{{ gradient }};--border:rgba(255,255,255,0.08);--font-heading:{{ font_heading }}}
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||||
|
header{position:sticky;top:0;background:rgba(15,7,32,0.85);backdrop-filter:blur(20px);border-bottom:1px solid var(--border)}
|
||||||
|
nav{max-width:900px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;padding:0.75rem 1.5rem}
|
||||||
|
.logo{font-family:var(--font-heading);font-weight:700;font-size:1.2rem;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-decoration:none}
|
||||||
|
main{max-width:900px;margin:0 auto;padding:3rem 1.5rem}
|
||||||
|
h1{font-family:var(--font-heading);font-size:2rem;margin-bottom:0.5rem}
|
||||||
|
h1 span{background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
|
.count{color:var(--text-muted);margin-bottom:2rem}
|
||||||
|
.card{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:0.75rem;text-decoration:none;display:block;color:inherit;transition:border-color 0.2s}
|
||||||
|
.card:hover{border-color:var(--primary)}
|
||||||
|
.card h3{font-size:1.05rem;margin-bottom:0.25rem}
|
||||||
|
.card p{color:var(--text-muted);font-size:0.88rem}
|
||||||
|
.card .meta{font-size:0.75rem;color:var(--text-muted);margin-top:0.5rem}
|
||||||
|
footer{border-top:1px solid var(--border);padding:2rem 1.5rem;text-align:center;color:var(--text-muted);font-size:0.8rem}
|
||||||
|
a{color:var(--accent)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><nav><a href="/" class="logo">{{ name }}</a></nav></header>
|
||||||
|
<main>
|
||||||
|
<h1>#<span>{{ tag }}</span></h1>
|
||||||
|
<p class="count">{{ articles|length }} article{% if articles|length != 1 %}s{% endif %}</p>
|
||||||
|
{% for a in articles %}
|
||||||
|
<a href="/articles/{{ a.slug }}" class="card">
|
||||||
|
<h3>{{ a.title }}</h3>
|
||||||
|
<p>{{ a.excerpt[:180] }}</p>
|
||||||
|
<div class="meta">{{ a.reading_time }} min read · {{ a.published_at[:10] }}</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not articles %}
|
||||||
|
<p style="color:var(--text-muted)">No articles tagged "{{ tag }}" yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer>© {{ domain }}</footer>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
NOT_FOUND_TEMPLATE = """<!DOCTYPE html>
|
NOT_FOUND_TEMPLATE = """<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
Reference in New Issue
Block a user