bestdadjokes/build.py
2026-09-21 22:15:42 +00:00

570 lines
29 KiB
Python
Raw Permalink 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 site generator for bestdadjokes.lol. Python 3.6 stdlib only."""
import json, os, re, shutil, html, datetime
ROOT = os.path.dirname(os.path.abspath(__file__))
CONTENT = os.path.join(ROOT, 'content', 'jokes.json')
SITE_DIR = os.path.join(ROOT, 'site')
STATIC = os.path.join(ROOT, 'static')
SITE_URL = 'https://bestdadjokes.lol'
SITE_NAME = 'Best Dad Jokes'
TAGLINE = 'The best dad jokes on the internet — short, clean, and groan-worthy.'
YEAR = datetime.date.today().year
with open(CONTENT) as f:
DATA = json.load(f)
CATS = DATA['categories']
JOKES = DATA['jokes']
CAT_BY_SLUG = {c['slug']: c for c in CATS}
for i, j in enumerate(JOKES):
j['id'] = i
# Long-tail topic pages: jokes matched by keyword across all categories.
TOPICS = [
{"slug": "coffee", "name": "Dad Jokes About Coffee", "emoji": "",
"intro": "Fuel for dads and their worst puns alike. These coffee dad jokes are best served strong, black, and with a healthy amount of eye-rolling.",
"keywords": r"\b(coffee|espresso|latte|caffeine|beans?|mugs?)\b"},
{"slug": "cats", "name": "Dad Jokes About Cats", "emoji": "🐱",
"intro": "For the cat dads out there. These cat dad jokes will make your feline walk out of the room in disgust — which, for a cat, is a standing ovation.",
"keywords": r"\b(cats?|kittens?|purr|feline|meow)\b"},
{"slug": "dogs", "name": "Dad Jokes About Dogs", "emoji": "🐕",
"intro": "A dog is the only audience that wags at dad jokes. Fetch! These dog dad jokes are good boys, every one of them.",
"keywords": r"\b(dogs?|puppy|puppies|pup|hound)\b"},
{"slug": "cows", "name": "Dad Jokes About Cows", "emoji": "🐄",
"intro": "Outstanding in their field and in our pun archive. These cow dad jokes are udderly relentless.",
"keywords": r"\b(cows?|cattle|moo|udder|beef|herd)\b"},
{"slug": "pizza", "name": "Dad Jokes About Pizza", "emoji": "🍕",
"intro": "A slice of comedy gold. These pizza dad jokes are cheesy by design — that's not a bug, it's the whole point.",
"keywords": r"\bpizzas?\b"},
{"slug": "money", "name": "Dad Jokes About Money", "emoji": "💰",
"intro": "Money can't buy happiness, but it can buy these money dad jokes, which are basically the same thing but cheaper.",
"keywords": r"\b(money|cash|coins?|dollars?|penn(y|ies)|bank|rich|spen[dt]|salary)\b"},
{"slug": "sleep", "name": "Dad Jokes About Sleep", "emoji": "😴",
"intro": "Read these sleep dad jokes in bed. Groaning yourself to sleep counts as a lullaby.",
"keywords": r"\b(sleep|nap|bed|pillow|blanket|tired|asleep|dream)\b"},
{"slug": "love", "name": "Dad Jokes About Love", "emoji": "❤️",
"intro": "Romance, dad-style. These love dad jokes have walked someone down the aisle against their will and lived to pun about it.",
"keywords": r"\b(love|romantic|valentine|marri(ed|age)|attract|wife|husband|buoy)\b"},
{"slug": "birthday", "name": "Birthday Dad Jokes", "emoji": "🎂",
"intro": "Because every birthday needs at least one groan with the cake. These birthday dad jokes are tradition now, sorry.",
"keywords": r"\b(birthday|cake|candles?|party|presents?)\b"},
{"slug": "winter", "name": "Winter Dad Jokes", "emoji": "",
"intro": "These winter dad jokes are snow joke. Perfect for breaking the ice at any cold-weather gathering.",
"keywords": r"\b(winter|snow|snowman|ic(y|icle)|frost|chill)\b"},
]
MIN_TOPIC_JOKES = 5
def esc(s):
return html.escape(s, quote=True)
def joke_count(slug):
return sum(1 for j in JOKES if j['cat'] == slug)
def joke_card(j, reveal=False):
"""A single joke card. Punchline is in the DOM (SEO-safe); JS blurs until clicked."""
cls = 'joke-card'
if j.get('type') == 'knock':
body = '<p class="joke-setup multi">%s</p>' % esc(j['setup']).replace('\n', '<br>')
else:
body = '<p class="joke-setup">%s</p>' % esc(j['setup'])
punch = '<div class="joke-punch" data-punch>%s</div>' % esc(j['punch'])
share = ('<button class="btn-tiny" data-share="%d">🖼️ Share as image</button>'
'<button class="btn-tiny" data-copy="%d">📋 Copy</button>' % (j['id'], j['id']))
return ('<figure class="%s" id="joke-%d">%s%s'
'<figcaption class="joke-actions">%s</figcaption></figure>'
% (cls, j['id'], body, punch, share))
def layout(title, desc, body, path='/', extra_head=''):
nav_cats = ''.join(
'<a href="/category/%s/">%s %s</a>' % (c['slug'], c['emoji'], esc(c['name']))
for c in CATS[:7])
rest = ''.join('<a href="/category/%s/">%s %s</a>' % (c['slug'], c['emoji'], esc(c['name']))
for c in CATS[7:])
foot_cats = ''.join('<a href="/category/%s/">%s</a>' % (c['slug'], esc(c['name'])) for c in CATS)
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">
<link rel="alternate" type="application/rss+xml" title="Dad Joke of the Day" href="/rss.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="website">
<meta property="og:url" content="{SITE_URL}{esc(path)}">
<meta name="twitter:card" content="summary">
<link rel="stylesheet" href="/static/style.css">
<style>:root{{--brand:#e4572e;--brand2:#f5a623;--ink:#2b2118;--cream:#fff8ec;--card:#ffffff}}</style>
{extra_head}
</head>
<body>
<header class="site-header">
<div class="wrap header-row">
<a class="logo" href="/">😂 <strong>Best<span>Dad</span>Jokes</strong><em>.lol</em></a>
<input type="checkbox" id="navtoggle" class="navtoggle">
<label for="navtoggle" class="navburger" aria-label="Menu">☰</label>
<nav class="site-nav">
<a href="/" {'class="on"' if path == '/' else ''}>Home</a>
<a href="/dad-joke-generator/" {'class="on"' if path.startswith('/dad-joke-generator') else ''}>Joke Generator</a>
<a href="/joke-of-the-day/" {'class="on"' if path.startswith('/joke-of-the-day') else ''}>Joke of the Day</a>
<a href="/search/" {'class="on"' if path.startswith('/search') else ''}>Search</a>
<a href="/about/" {'class="on"' if path.startswith('/about') else ''}>About</a>
</nav>
</div>
<div class="wrap catbar">
{nav_cats}<a href="#all-cats">more →</a>
</div>
</header>
<main>
{body}
</main>
<footer class="site-footer">
<div class="wrap foot-grid">
<div>
<p class="foot-logo">😂 Best<span>Dad</span>Jokes<em>.lol</em></p>
<p>Hi, I'm a dad. These are my jokes. Someone has to laugh at them, and it might as well be the internet.</p>
</div>
<div>
<p class="foot-h">Categories</p>
<div class="foot-links">{foot_cats}</div>
</div>
<div>
<p class="foot-h">More</p>
<div class="foot-links">
<a href="/dad-joke-generator/">Dad Joke Generator</a>
<a href="/joke-of-the-day/">Joke of the Day</a>
<a href="/search/">Search Jokes</a>
<a href="/rss.xml">RSS Feed</a>
<a href="/api/v1/random.json">Free JSON API</a>
<a href="/about/">About</a>
<a href="/contact/">Contact</a>
<a href="/privacy-policy/">Privacy Policy</a>
<a href="/terms/">Terms</a>
</div>
</div>
</div>
<div class="wrap foot-base">
<p>© {YEAR} {SITE_NAME} · Family-friendly jokes, dad-certified since the Big Dad Energy era.</p>
</div>
</footer>
<script src="/static/jokes-data.js" defer></script>
<script src="/static/main.js" defer></script>
</body>
</html>"""
def page_index():
rnd = JOKES[(datetime.date.today().toordinal() * 7) % len(JOKES)]
d_idx = datetime.date.today().toordinal()
jod = JOKES[d_idx % len(JOKES)]
latest = JOKES[-12:][::-1]
live_topics = [t for t in TOPICS if len(topic_jokes(t)) >= MIN_TOPIC_JOKES]
topic_chips = ''.join('<a class="chip" href="/topic/%s/">%s %s</a>' % (
t['slug'], t['emoji'], esc(t['name'].replace('Dad Jokes About ', '').replace('Dad Jokes', 'Jokes')))
for t in live_topics)
cat_cards = ''.join(f"""
<a class="cat-card" href="/category/{c['slug']}/">
<span class="cat-emoji">{c['emoji']}</span>
<span class="cat-name">{esc(c['name'])}</span>
<span class="cat-count">{joke_count(c['slug'])} jokes</span>
</a>""" for c in CATS)
latest_cards = ''.join(joke_card(j) for j in latest)
body = f"""
<section class="hero wrap">
<h1>The Best Dad Jokes on the Internet</h1>
<p class="hero-sub">Short. Clean. Painfully punny. {len(JOKES)}+ jokes your kids don't want to hear but absolutely need to.</p>
<div class="hero-card">
<p class="joke-setup" data-hero-setup>{esc(rnd['setup'])}</p>
<div class="joke-punch" data-hero-punch>{esc(rnd['punch'])}</div>
<div class="hero-btns">
<button class="btn" data-hero-next>🎲 Another one</button>
<button class="btn btn-ghost" data-copy-hero>📋 Copy</button>
<button class="btn btn-ghost" data-share-hero>🖼️ Share as image</button>
</div>
</div>
</section>
<!-- AD_SLOT_TOP -->
<section class="wrap">
<div class="jod-banner">
<p class="jod-label">😄 Dad Joke of the Day</p>
<p class="joke-setup">{esc(jod['setup'])}</p>
<div class="joke-punch" data-punch>{esc(jod['punch'])}</div>
<p class="jod-more"><a href="/joke-of-the-day/">See the archive →</a></p>
</div>
</section>
<section class="wrap" id="all-cats">
<h2>Browse Dad Jokes by Category</h2>
<div class="cat-grid">{cat_cards}</div>
</section>
<section class="wrap" id="topics">
<h2>Popular Dad Joke Topics</h2>
<div class="chips">
{topic_chips}
</div>
</section>
<!-- AD_SLOT_MID -->
<section class="wrap">
<h2>Fresh Out of the Dad Oven</h2>
<div class="joke-grid">{latest_cards}</div>
<p class="center-link"><a class="btn" href="/dad-joke-generator/">Open the Dad Joke Generator →</a></p>
</section>
<section class="wrap seo-blurb">
<h2>What is a dad joke?</h2>
<p>A dad joke is a short, clean, pun-based joke delivered with maximum confidence and zero shame. It's the kind of joke that makes your kids groan, your wife roll her eyes, and your dog leave the room. Science has yet to explain why hearing one makes you feel briefly smarter and visibly worse at the same time.</p>
<p>This site is a growing collection of the best dad jokes on the internet: one-liners, knock-knock jokes, and puns about animals, food, work, school, sports, tech, space and more. Every joke is family-friendly — the only thing at risk of injury is everyone's patience. Start with the <a href="/dad-joke-generator/">dad joke generator</a>, browse the <a href="#all-cats">categories</a>, or come back every day for a new <a href="/joke-of-the-day/">dad joke of the day</a>.</p>
</section>"""
title = 'Best Dad Jokes — %d+ Funny, Clean Dad Jokes (One-Liners, Puns & Knock-Knocks)' % len(JOKES)
desc = ('The best dad jokes on the internet: %d clean, funny, groan-worthy dad jokes. '
'One-liners, knock-knock jokes and puns by category, plus a daily dad joke and a free generator.'
% len(JOKES))
ld = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": SITE_NAME,
"url": SITE_URL + '/',
"description": TAGLINE,
"potentialAction": {
"@type": "SearchAction",
"target": SITE_URL + "/search/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
return layout(title, desc, body, '/',
'<script type="application/ld+json">%s</script>' % esc(json.dumps(ld)))
def page_category(slug):
c = CAT_BY_SLUG[slug]
js = [j for j in JOKES if j['cat'] == slug]
cards = ''.join(joke_card(j) for j in js)
others = [o for o in CATS if o['slug'] != slug][:6]
others_html = ''.join('<a class="chip" href="/category/%s/">%s %s</a>' % (o['slug'], o['emoji'], esc(o['name'])) for o in others)
body = f"""
<div class="wrap">
<nav class="crumbs"><a href="/">Home</a> <span>{esc(c['name'])}</span></nav>
<h1>{c['emoji']} {esc(c['name'])}</h1>
<p class="page-sub">{esc(c['blurb'])} That's {len(js)} jokes of pure dad energy.</p>
<div class="joke-grid">{cards}</div>
<!-- AD_SLOT_CAT -->
<h2>Keep the groans going</h2>
<div class="chips">{others_html}</div>
<p class="center-link"><a class="btn" href="/dad-joke-generator/">🎲 Random Dad Joke Generator</a></p>
</div>"""
title = '%s%d Funny, Clean %s' % (c['name'], len(js), c['name'])
desc = '%s %s %d jokes, all clean and all terrible. Perfect for dads, teachers and any occasion that needs a groan.' % (c['blurb'], 'Browse', len(js))
ld = {"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": SITE_URL + '/'},
{"@type": "ListItem", "position": 2, "name": c['name'], "item": SITE_URL + '/category/%s/' % slug}]}
return layout(title, desc, body, '/category/%s/' % slug,
'<script type="application/ld+json">%s</script>' % esc(json.dumps(ld)))
def topic_jokes(topic):
pat = re.compile(topic['keywords'])
return [j for j in JOKES if pat.search((j['setup'] + ' ' + j['punch']).lower())]
def page_topic(slug):
t = next(t for t in TOPICS if t['slug'] == slug)
js = topic_jokes(t)
if len(js) < MIN_TOPIC_JOKES:
return None
cards = ''.join(joke_card(j) for j in js)
other_topics = [o for o in TOPICS if o['slug'] != slug and len(topic_jokes(o)) >= MIN_TOPIC_JOKES][:6]
chips = ''.join('<a class="chip" href="/topic/%s/">%s %s</a>' % (o['slug'], o['emoji'], esc(o['name'].replace('Dad Jokes About ', '').replace('Dad Jokes', 'Jokes'))) for o in other_topics)
body = f"""
<div class="wrap">
<nav class="crumbs"><a href="/">Home</a> <a href="/#topics">Topics</a> <span>{esc(t['name'])}</span></nav>
<h1>{t['emoji']} {esc(t['name'])}</h1>
<p class="page-sub">{esc(t['intro'])} That's {len(js)} of them, carefully curated for maximum groan.</p>
<div class="joke-grid">{cards}</div>
<h2>More where that came from</h2>
<div class="chips">{chips}</div>
<p class="center-link"><a class="btn" href="/dad-joke-generator/">🎲 Random Dad Joke Generator</a></p>
</div>"""
title = '%s%d Clean, Groan-Worthy Jokes' % (t['name'], len(js))
desc = '%s %s' % (t['intro'], 'A curated collection of %d clean, family-friendly jokes.' % len(js))
return layout(title, desc, body, '/topic/%s/' % slug)
def page_generator():
body = """
<div class="wrap narrow">
<h1>🎲 Dad Joke Generator</h1>
<p class="page-sub">One click. One random dad joke. Zero apologies. Works at parties, in the car line, and mid-argument.</p>
<div class="hero-card gen-card">
<p class="joke-setup" data-gen-setup>Click the button to summon a dad joke.</p>
<div class="joke-punch" data-gen-punch></div>
<div class="hero-btns">
<button class="btn" data-gen-next>🎲 Generate dad joke</button>
<button class="btn btn-ghost" data-gen-copy>📋 Copy</button>
<button class="btn btn-ghost" data-gen-share>🖼️ Share as image</button>
</div>
</div>
<h2>How to weaponize a dad joke</h2>
<ol class="plain-list">
<li><strong>Deliver with confidence.</strong> Eye contact is everything. The dad joke fears hesitation.</li>
<li><strong>Pause before the punchline.</strong> Let the suspense marinate. Then pounce.</li>
<li><strong>Ignore the groans.</strong> The groan is applause in dad dialect.</li>
<li><strong>Repeat the best ones.</strong> Repetition builds tradition. Tradition builds groans.</li>
</ol>
<p>Looking for something specific? <a href="/search/">Search all dad jokes</a> or browse by <a href="/#all-cats">category</a>.</p>
</div>"""
title = 'Dad Joke Generator — Random Dad Jokes, One Click'
desc = 'Press one button and get a random dad joke instantly. Hundreds of clean, funny dad jokes ready to embarrass your kids.'
return layout(title, desc, body, '/dad-joke-generator/')
def page_jotd():
d = datetime.date.today()
j = JOKES[d.toordinal() % len(JOKES)]
body = f"""
<div class="wrap narrow">
<h1>😄 Dad Joke of the Day</h1>
<p class="page-sub">A brand-new dad joke every single day. Doctors recommend exactly one per day. We won't tell if you don't.</p>
<div class="jod-banner jod-page">
<p class="jod-date" data-jod-date>{d.strftime('%B %d, %Y')}</p>
<p class="joke-setup" data-jod-setup>{esc(j['setup'])}</p>
<div class="joke-punch" data-punch data-jod-punch>{esc(j['punch'])}</div>
<div class="hero-btns">
<button class="btn btn-ghost" data-jod-copy>📋 Copy</button>
<button class="btn btn-ghost" data-jod-share>🖼️ Share as image</button>
</div>
</div>
<h2>The dad joke archive</h2>
<p>Every recent dad joke of the day, one per day, no repeats (for a few months, anyway):</p>
<div class="archive" data-jod-archive></div>
<p class="center-link"><a class="btn" href="/dad-joke-generator/">Can't wait a whole day? Generator →</a></p>
</div>"""
title = 'Dad Joke of the Day — A New Funny Dad Joke Daily'
desc = "Today's dad joke of the day, plus the full archive. One clean, funny dad joke for every day of the year."
ld = {"@context": "https://schema.org", "@type": "SpecialAnnouncement", "name": "Dad Joke of the Day",
"url": SITE_URL + '/joke-of-the-day/', "category": "https://schema.org/EntertainmentEvent"}
return layout(title, desc, body, '/joke-of-the-day/',
'<link rel="canonical" href="%s/joke-of-the-day/">' % SITE_URL +
'<script type="application/ld+json">%s</script>' % esc(json.dumps(ld)))
def page_search():
body = """
<div class="wrap narrow">
<h1>🔎 Search Dad Jokes</h1>
<p class="page-sub">Looking for a dad joke about coffee, cats, or cardio? Type it in. We probably have a pun for that.</p>
<form class="search-form" onsubmit="return false;">
<input type="search" id="search-box" placeholder="Try 'pizza', 'cow', 'work'..." autocomplete="off">
</form>
<p class="search-count" data-search-count></p>
<div class="joke-grid" data-search-results></div>
<p data-search-empty style="display:none">No jokes matched. Which, honestly, is a great setup for a new dad joke — send it to us via the <a href="/contact/">contact page</a>.</p>
</div>"""
title = 'Search Dad Jokes — Find a Dad Joke About Anything'
desc = 'Search hundreds of clean dad jokes by keyword. Find puns and one-liners about any topic instantly.'
return layout(title, desc, body, '/search/')
def page_about():
body = """
<div class="wrap narrow">
<h1>👋 About Best Dad Jokes</h1>
<p>Welcome to <strong>BestDadJokes.lol</strong> — a growing collection of the internet's best dad jokes: short, clean, pun-heavy, and delivered with the quiet confidence only a dad can muster.</p>
<p>The mission is simple: build the most useful dad joke resource on the internet. That means:</p>
<ul class="plain-list">
<li><strong>Curated, not scraped.</strong> Every joke is hand-picked for maximum groan. If it doesn't make someone audibly sigh, it doesn't make the cut.</li>
<li><strong>Family-friendly, always.</strong> Clean humor that's safe to tell at the dinner table, the office, or the school pickup line.</li>
<li><strong>Fast and simple.</strong> No pop-ups, no logins, no nonsense. Just jokes.</li>
</ul>
<h2>Why dad jokes?</h2>
<p>Because a dad joke is never just a joke. It's a love language. It's saying "I would rather embarrass myself than miss a chance to make you laugh." Also, they're free, and this is a budget operation.</p>
<h2>Get in touch</h2>
<p>Got a joke that deserves to be here? Found a bug? Want to license 300 dad jokes for your family group chat? Head to the <a href="/contact/">contact page</a>.</p>
</div>"""
title = 'About — Best Dad Jokes .lol'
desc = 'BestDadJokes.lol is a curated collection of clean, funny dad jokes. Hand-picked jokes, family-friendly humor, zero nonsense.'
return layout(title, desc, body, '/about/')
def page_contact():
body = """
<div class="wrap narrow">
<h1>📮 Contact</h1>
<p>Want to send us the greatest dad joke of all time? Report a joke that's more "dad" than "joke"? Tell us the site made you laugh so hard you spilled your coffee? We read everything.</p>
<p>Email us at <a href="mailto:hello@bestdadjokes.lol">hello@bestdadjokes.lol</a> — responses may be delayed by naps.</p>
<h2>Joke submissions</h2>
<p>Include the setup and the punchline in your email. If we use it, you get full bragging rights and absolutely no money, which is exactly how dad economics works.</p>
</div>"""
title = 'Contact — Best Dad Jokes .lol'
desc = 'Get in touch with BestDadJokes.lol — submit a dad joke, say hi, or report a bug.'
return layout(title, desc, body, '/contact/')
def page_privacy():
body = """
<div class="wrap narrow">
<h1>Privacy Policy</h1>
<p class="page-sub">Last updated: %s</p>
<p>BestDadJokes.lol respects your privacy, which is ironic because dads famously respect nothing more than a good interruption. Here's the deal in plain English:</p>
<h2>What we collect</h2>
<p>We keep standard web server logs (IP address, browser type, pages visited) for security and aggregate traffic statistics. We do not ask for your name, email, or your kids' report cards. There are no accounts on this site.</p>
<h2>Cookies and advertising</h2>
<p>We may display advertising on this site to keep it 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.</p>
<p>Google's use of advertising cookies enables it and its partners to serve ads based on your visits to this site and/or other sites on the Internet. You may opt out of personalized advertising by visiting <a href="https://www.google.com/settings/ads" rel="nofollow noopener">Google Ads Settings</a>, or opt out of third-party vendor cookies at <a href="https://www.aboutads.info" rel="nofollow noopener">aboutads.info</a>.</p>
<p>If we run a consent banner for visitors from the EEA/UK, your choices will be honored and stored locally.</p>
<h2>Analytics</h2>
<p>We use aggregate, privacy-respecting analytics to count page views and understand which categories people enjoy. This data cannot reasonably be used to identify you.</p>
<h2>Data sharing</h2>
<p>We do not sell personal data. We have no personal data to sell. We barely have data.</p>
<h2>Children</h2>
<p>Content on this site is family-friendly by design. The site is not directed at children under 13, and we do not knowingly collect personal information from children.</p>
<h2>Contact</h2>
<p>Questions about this policy? Email <a href="mailto:hello@bestdadjokes.lol">hello@bestdadjokes.lol</a>.</p>
</div>""" % datetime.date.today().strftime('%B %d, %Y')
title = 'Privacy Policy — Best Dad Jokes .lol'
desc = 'Privacy policy for BestDadJokes.lol — what we collect (very little), cookies, advertising, and your choices.'
return layout(title, desc, 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 BestDadJokes.lol you agree to the following terms. Reading them aloud in a dad voice is optional but encouraged.</p>
<h2>Use of content</h2>
<p>The jokes on this site are provided for personal, non-commercial enjoyment. Share them freely at dinner tables, offices, and group chats. If you want to republish jokes in bulk or use this site commercially, contact us first — we're reasonable people.</p>
<h2>No warranties</h2>
<p>This site is provided "as is." We make no guarantees that jokes will be funny, that groans will be produced, or that your teenagers will acknowledge your existence. All comedic risk is yours.</p>
<h2>Limitation of liability</h2>
<p>To the fullest extent permitted by law, BestDadJokes.lol is not liable for any damages arising from use of the site, including but not limited to eye-rolling, sighing, involuntary chuckling, and children hiding your phone.</p>
<h2>External links</h2>
<p>We are not responsible for the content of external websites, including their jokes, which are almost certainly worse than ours.</p>
<h2>Changes</h2>
<p>We may update these terms occasionally. Continued use of the site means you accept the updated terms, and possibly a fresh dad joke.</p>
</div>""" % datetime.date.today().strftime('%B %d, %Y')
title = 'Terms of Use — Best Dad Jokes .lol'
desc = 'Terms of use for BestDadJokes.lol.'
return layout(title, desc, body, '/terms/')
def page_404():
body = """
<div class="wrap narrow center">
<h1>404</h1>
<p class="page-sub">This page is like a dad joke that got cut from the site — it just didn't land.</p>
<div class="hero-card">
<p class="joke-setup">Why did the web page go to therapy?</p>
<div class="joke-punch" data-punch>It had too many broken links.</div>
</div>
<p><a class="btn" href="/">Take me home →</a></p>
</div>"""
return layout('404 — Page Not Found', 'Page not found. The joke is on us.', body, '/404.html')
def write(path, content):
full = os.path.join(SITE_DIR, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, 'w') as f:
f.write(content)
def build():
if os.path.exists(SITE_DIR):
shutil.rmtree(SITE_DIR)
os.makedirs(SITE_DIR)
# static assets
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)
# jokes data for client-side JS
payload = {
'categories': [{'slug': c['slug'], 'name': c['name'], 'emoji': c['emoji']} for c in CATS],
'jokes': [{'id': j['id'], 'cat': j['cat'], 'type': j.get('type', 'qa'),
's': j['setup'], 'p': j['punch']} for j in JOKES]}
write('static/jokes-data.js',
'window.BDJ=%s;' % json.dumps(payload, ensure_ascii=False, separators=(',', ':')))
write('index.html', page_index())
for c in CATS:
write('category/%s/index.html' % c['slug'], page_category(c['slug']))
for t in TOPICS:
page = page_topic(t['slug'])
if page:
write('topic/%s/index.html' % t['slug'], page)
write('dad-joke-generator/index.html', page_generator())
write('joke-of-the-day/index.html', page_jotd())
write('search/index.html', page_search())
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())
# robots + sitemap
pages = ['/', '/dad-joke-generator/', '/joke-of-the-day/', '/search/', '/about/', '/contact/',
'/privacy-policy/', '/terms/'] + ['/category/%s/' % c['slug'] for c in CATS] \
+ ['/topic/%s/' % t['slug'] for t in TOPICS if len(topic_jokes(t)) >= MIN_TOPIC_JOKES]
write('robots.txt', 'User-agent: *\nAllow: /\n\nSitemap: %s/sitemap.xml\n' % SITE_URL)
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><changefreq>%s</changefreq><priority>%s</priority></url>'
% (SITE_URL, p, today, 'weekly' if p == '/' else 'monthly', '1.0' if p == '/' else '0.8'))
sm.append('</urlset>')
write('sitemap.xml', '\n'.join(sm))
# RSS: dad joke of the day feed, last 21 days
from email.utils import formatdate
now_ts = datetime.datetime.utcnow().timestamp()
items = []
for back in range(21):
d = datetime.date.today() - datetime.timedelta(days=back)
j = JOKES[d.toordinal() % len(JOKES)]
ts = now_ts - back * 86400
items.append(
'<item><title>%s</title><link>%s/joke-of-the-day/?d=%s</link>'
'<guid isPermaLink="false">bjd-daily-%s</guid>'
'<pubDate>%s</pubDate><description>%s%s</description></item>'
% (esc('Dad Joke of the Day — %s' % d.strftime('%b %d')), SITE_URL, d.isoformat(),
d.isoformat(), formatdate(ts), esc(j['setup']), esc(j['punch'])))
rss = ('<?xml version="1.0" encoding="UTF-8"?>'
'<rss version="2.0"><channel><title>%s — Dad Joke of the Day</title>'
'<link>%s/joke-of-the-day/</link><description>%s</description>'
'<language>en-us</language>%s</channel></rss>'
% (esc(SITE_NAME), SITE_URL, esc(TAGLINE), ''.join(items)))
write('rss.xml', rss)
# free JSON API
rnd = JOKES[(datetime.datetime.utcnow().microsecond // 1000) * len(JOKES) // 1000]
tod = JOKES[datetime.date.today().toordinal() % len(JOKES)]
def api_j(j):
return json.dumps({'setup': j['setup'], 'punchline': j['punch'], 'type': j.get('type', 'qa'),
'category': j['cat'],
'source': SITE_URL + '/category/%s/#joke-%d' % (j['cat'], j['id'])},
ensure_ascii=False)
write('api/v1/random.json', api_j(rnd))
write('api/v1/today.json', api_j(tod))
total = sum(os.path.getsize(os.path.join(dp, f)) for dp, dn, fn in os.walk(SITE_DIR) for f in fn)
nfiles = sum(len(fn) for dp, dn, fn in os.walk(SITE_DIR))
print('Built %d files into %s (%d bytes)' % (nfiles, SITE_DIR, total))
if __name__ == '__main__':
build()