devops/helpme-tips/build.py

297 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Static generator for helpme.tips — arbitrage-ready content site.
Articles are multi-page (pagination = more ad impressions per session).
Python 3.6 stdlib only. Ad slots are emitted as <!-- AD_SLOT_* --> comments
plus empty mount divs; the fleet ad wrapper fills them.
"""
import json, os, shutil, html, datetime
ROOT = os.path.dirname(os.path.abspath(__file__))
SITE_DIR = os.path.join(ROOT, 'site')
STATIC = os.path.join(ROOT, 'static')
SITE_URL = 'https://helpme.tips'
SITE_NAME = 'Help Me Tips'
YEAR = datetime.date.today().year
def esc(s):
return html.escape(s, quote=True)
def load_articles():
with open(os.path.join(ROOT, 'content', 'articles.json')) as f:
arts = json.load(f)
for i, a in enumerate(arts):
a['id'] = i
return arts
def layout(title, desc, body, path='/', extra_head=''):
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{esc(title)}</title>
<meta name="description" content="{esc(desc)}">
<link rel="canonical" href="{SITE_URL}{esc(path)}">
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<meta property="og:site_name" content="{SITE_NAME}">
<meta property="og:title" content="{esc(title)}">
<meta property="og:description" content="{esc(desc)}">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary">
{extra_head}
</head>
<body>
<header class="site-header">
<div class="wrap head-row">
<a class="logo" href="/">Help<span>Me</span>Tips</a>
<nav class="nav">
<a href="/">Home</a>
<a href="/#latest">Latest</a>
<a href="/about/">About</a>
<a href="/contact/">Contact</a>
</nav>
</div>
</header>
<!-- AD_SLOT_HEADER -->
<main>
{body}
</main>
<footer class="site-footer">
<div class="wrap">
<p class="foot-logo">Help<span>Me</span>Tips</p>
<p>Practical tips for everyday life — home, sleep, coffee, habits and more. No fluff, no sales pitches.</p>
<div class="foot-links">
<a href="/about/">About</a>
<a href="/contact/">Contact</a>
<a href="/privacy-policy/">Privacy Policy</a>
<a href="/terms/">Terms</a>
</div>
<p class="tiny"{YEAR} {SITE_NAME}. All articles are for general information only and are not professional advice.</p>
</div>
</footer>
<script src="/static/main.js" defer></script>
</body>
</html>"""
def article_card(a):
return f"""
<a class="card" href="/tips/{a['slug']}/">
<span class="cat">{esc(a['category'])}</span>
<h3>{esc(a['title'])}</h3>
<p>{esc(a['dek'])}</p>
<span class="meta">{a['pages_count']} pages · {a.get('read_min', 4)} min read</span>
</a>"""
def page_index(articles):
cards = ''.join(article_card(a) for a in articles)
body = f"""
<section class="hero wrap">
<h1>Tips that actually help.</h1>
<p class="hero-sub">Short, practical guides for everyday problems — kitchen, sleep, coffee, habits, home and your phone. No fluff, no 800-word intros.</p>
</section>
<!-- AD_SLOT_TOP -->
<section class="wrap" id="latest">
<h2>Latest tips</h2>
<div class="grid">{cards}</div>
</section>
<!-- AD_SLOT_MID -->
<section class="wrap seo">
<h2>Why Help Me Tips?</h2>
<p>Most tip articles online bury one useful sentence under a wall of filler. We do the opposite: every page is a tight, actionable list you can finish with your coffee. New guides are added every week across home, kitchen, sleep, tech and habits.</p>
</section>"""
title = 'Help Me Tips — Practical Everyday Tips That Actually Help'
desc = ('Short, practical guides for everyday life: kitchen shortcuts, better sleep, '
'coffee, phone settings, habits and home. New tips weekly.')
return layout(title, desc, body, '/')
def page_article(a, page_idx, total):
pg = a['pages'][page_idx]
parts = []
if pg.get('heading'):
parts.append('<h2>%s</h2>' % esc(pg['heading']))
parts += ['<p>%s</p>' % esc(p) for p in pg['paras']]
for h, paras in pg.get('sections', []):
parts.append('<h3>%s</h3>' % esc(h))
parts += ['<p>%s</p>' % esc(p) for p in paras]
# multi-item list support
content = '\n'.join(parts)
nav = []
if page_idx > 0:
prev = '/' if page_idx == 1 else '/tips/%s/%d/' % (a['slug'], page_idx)
nav.append('<a class="btn ghost" href="%s">← Previous</a>' % prev)
nav.append('<span class="pageno">Page %d of %d</span>' % (page_idx + 1, total))
if page_idx < total - 1:
nav.append('<a class="btn" href="/tips/%s/%d/">Next page →</a>' % (a['slug'], page_idx + 2))
nav_html = '<div class="pager">%s</div>' % '\n'.join(nav)
dots = '<div class="dots">%s</div>' % ''.join(
'<span class="%s"></span>' % ('on' if i == page_idx else '') for i in range(total))
body = f"""
<article class="wrap article">
<nav class="crumbs"><a href="/">Home</a> <span>{esc(a['category'])}</span></nav>
<h1>{esc(a['title'])}</h1>
<p class="dek">{esc(a['dek'])}</p>
<p class="meta">{a.get('updated', '')} · Page {page_idx + 1} of {total} · {a.get('read_min', 4)} min read</p>
<!-- AD_SLOT_ARTICLE_TOP -->
<div class="content">
{content}
</div>
{dots}
<!-- AD_SLOT_ARTICLE_MID -->
{nav_html}
<!-- AD_SLOT_ARTICLE_BOTTOM -->
<div class="more">
<h2>Keep reading</h2>
<div class="grid">{more_cards(a, articles_all)}</div>
</div>
</article>"""
suffix = '' if page_idx == 0 else ' (Page %d)' % (page_idx + 1)
title = '%s%s — Help Me Tips' % (a['title'], suffix)
desc = a['dek']
ld = {"@context": "https://schema.org", "@type": "Article", "headline": a['title'],
"description": a['dek'], "mainEntityOfPage": SITE_URL + '/tips/%s/' % a['slug'],
"datePublished": a.get('updated', '2026-09-16')}
extra = '<script type="application/ld+json">%s</script>' % esc(json.dumps(ld))
path = '/tips/%s/' % a['slug'] if page_idx == 0 else '/tips/%s/%d/' % (a['slug'], page_idx + 1)
return layout(title, desc, body, path, extra)
def more_cards(current, all_arts):
others = [a for a in all_arts if a['slug'] != current['slug']][:4]
return ''.join(article_card(a) for a in others)
articles_all = [] # populated in build(); used by more_cards
def page_about():
body = """
<div class="wrap narrow">
<h1>About Help Me Tips</h1>
<p>Help Me Tips publishes short, practical guides for everyday problems. We believe a good tip article should respect your time: clear steps, no 800-word personal essays, no product pitches.</p>
<p>Topics we cover: kitchen and cooking shortcuts, sleep quality, coffee, phone and tech settings, home care, and small habits that compound.</p>
<h2>Our editorial standard</h2>
<ul class="plain">
<li>Every tip must be actionable today with things you already own.</li>
<li>No medical, legal or financial claims. General information only.</li>
<li>If a tip doesn't survive being tested in a real home, it doesn't ship.</li>
</ul>
</div>"""
return layout('About — Help Me Tips', 'What Help Me Tips is and how we pick tips.', body, '/about/')
def page_contact():
body = """
<div class="wrap narrow">
<h1>Contact</h1>
<p>Tip ideas, corrections, feedback? We read everything: <a href="mailto:hello@helpme.tips">hello@helpme.tips</a></p>
</div>"""
return layout('Contact — Help Me Tips', 'Contact Help Me Tips.', body, '/contact/')
def page_privacy():
body = """
<div class="wrap narrow">
<h1>Privacy Policy</h1>
<p class="page-sub">Last updated: %s</p>
<p>Help Me Tips collects standard web server logs (IP address, browser type, pages visited) for security and aggregate statistics. We do not require accounts and do not collect names or emails.</p>
<h2>Cookies and advertising</h2>
<p>We display advertising to keep this site free. Advertising partners, which may include Google, use cookies and similar technologies to serve ads based on your prior visits to this and other websites. You may opt out of personalized advertising at <a rel="nofollow noopener" href="https://www.google.com/settings/ads">Google Ads Settings</a> or <a rel="nofollow noopener" href="https://www.aboutads.info">aboutads.info</a>.</p>
<p>Where required by law (EEA/UK), a consent banner will let you accept or reject non-essential cookies, and your choice will be honored.</p>
<h2>Data sharing</h2>
<p>We do not sell personal information. Analytics, if used, are aggregate and not identifiable.</p>
<h2>Contact</h2>
<p>Questions: <a href="mailto:hello@helpme.tips">hello@helpme.tips</a></p>
</div>""" % datetime.date.today().strftime('%B %d, %Y')
return layout('Privacy Policy — Help Me Tips', 'Privacy policy for helpme.tips.', body, '/privacy-policy/')
def page_terms():
body = """
<div class="wrap narrow">
<h1>Terms of Use</h1>
<p class="page-sub">Last updated: %s</p>
<p>By using helpme.tips you agree to these terms. Content is provided for general information only and is not professional, medical, legal or financial advice. Use tips at your own discretion.</p>
<h2>Use of content</h2>
<p>Personal, non-commercial use only. Bulk republication requires written permission.</p>
<h2>No warranties</h2>
<p>The site is provided "as is" without warranties of any kind. Results of following tips may vary.</p>
<h2>Contact</h2>
<p>Questions: <a href="mailto:hello@helpme.tips">hello@helpme.tips</a></p>
</div>""" % datetime.date.today().strftime('%B %d, %Y')
return layout('Terms of Use — Help Me Tips', 'Terms for helpme.tips.', body, '/terms/')
def page_404():
body = """
<div class="wrap narrow center">
<h1>404</h1>
<p class="page-sub">That page slipped out of the tips drawer.</p>
<p><a class="btn" href="/">Back to all tips →</a></p>
</div>"""
return layout('404 — Help Me Tips', 'Page not found.', body, '/404.html')
def write(path, content):
full = os.path.join(SITE_DIR, path)
os.makedirs(os.path.dirname(full) or SITE_DIR, exist_ok=True)
with open(full, 'w') as f:
f.write(content)
def build():
global articles_all
if os.path.exists(SITE_DIR):
shutil.rmtree(SITE_DIR)
os.makedirs(SITE_DIR)
st = os.path.join(SITE_DIR, 'static')
os.makedirs(st, exist_ok=True)
for fn in os.listdir(STATIC):
shutil.copy(os.path.join(STATIC, fn), st)
articles = load_articles()
articles_all = articles
articles.sort(key=lambda a: a.get('updated', ''), reverse=True)
for a in articles:
a['pages_count'] = len(a['pages'])
write('index.html', page_index(articles))
n_pages = 0
for a in articles:
total = len(a['pages'])
a['pages_count'] = total
for i in range(total):
write('tips/%s/index.html' % a['slug'] if i == 0
else 'tips/%s/%d/index.html' % (a['slug'], i + 1),
page_article(a, i, total))
n_pages += 1
write('about/index.html', page_about())
write('contact/index.html', page_contact())
write('privacy-policy/index.html', page_privacy())
write('terms/index.html', page_terms())
write('404.html', page_404())
pages = ['/', '/about/', '/contact/', '/privacy-policy/', '/terms/'] + \
['/tips/%s/' % a['slug'] for a in articles]
today = datetime.date.today().isoformat()
sm = ['<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
for p in pages:
sm.append('<url><loc>%s%s</loc><lastmod>%s</lastmod></url>' % (SITE_URL, p, today))
sm.append('</urlset>')
write('sitemap.xml', '\n'.join(sm))
write('robots.txt', 'User-agent: *\nAllow: /\n\nSitemap: %s/sitemap.xml\n' % SITE_URL)
nfiles = sum(len(fn) for _, _, fn in os.walk(SITE_DIR))
total_bytes = sum(os.path.getsize(os.path.join(dp, f)) for dp, _, fn in os.walk(SITE_DIR) for f in fn)
print('Built %d files (%d article pages, %d articles) -> %s (%d bytes)'
% (nfiles, n_pages, len(articles), SITE_DIR, total_bytes))
if __name__ == '__main__':
build()