#!/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 = '

%s

' % esc(j['setup']).replace('\n', '
') else: body = '

%s

' % esc(j['setup']) punch = '
%s
' % esc(j['punch']) share = ('' '' % (j['id'], j['id'])) return ('
%s%s' '
%s
' % (cls, j['id'], body, punch, share)) def layout(title, desc, body, path='/', extra_head=''): nav_cats = ''.join( '%s %s' % (c['slug'], c['emoji'], esc(c['name'])) for c in CATS[:7]) rest = ''.join('%s %s' % (c['slug'], c['emoji'], esc(c['name'])) for c in CATS[7:]) foot_cats = ''.join('%s' % (c['slug'], esc(c['name'])) for c in CATS) return f""" {esc(title)} {extra_head}
{body}
""" 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('%s %s' % ( t['slug'], t['emoji'], esc(t['name'].replace('Dad Jokes About ', '').replace('Dad Jokes', 'Jokes'))) for t in live_topics) cat_cards = ''.join(f""" {c['emoji']} {esc(c['name'])} {joke_count(c['slug'])} jokes """ for c in CATS) latest_cards = ''.join(joke_card(j) for j in latest) body = f"""

The Best Dad Jokes on the Internet

Short. Clean. Painfully punny. {len(JOKES)}+ jokes your kids don't want to hear but absolutely need to.

{esc(rnd['setup'])}

{esc(rnd['punch'])}

๐Ÿ˜„ Dad Joke of the Day

{esc(jod['setup'])}

{esc(jod['punch'])}

See the archive โ†’

Browse Dad Jokes by Category

{cat_cards}

Popular Dad Joke Topics

{topic_chips}

Fresh Out of the Dad Oven

{latest_cards}

What is a dad joke?

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.

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 dad joke generator, browse the categories, or come back every day for a new dad joke of the day.

""" 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, '/', '' % 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('%s %s' % (o['slug'], o['emoji'], esc(o['name'])) for o in others) body = f"""

{c['emoji']} {esc(c['name'])}

{esc(c['blurb'])} That's {len(js)} jokes of pure dad energy.

{cards}

Keep the groans going

{others_html}
""" 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, '' % 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('%s %s' % (o['slug'], o['emoji'], esc(o['name'].replace('Dad Jokes About ', '').replace('Dad Jokes', 'Jokes'))) for o in other_topics) body = f"""

{t['emoji']} {esc(t['name'])}

{esc(t['intro'])} That's {len(js)} of them, carefully curated for maximum groan.

{cards}

More where that came from

{chips}
""" 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 = """

๐ŸŽฒ Dad Joke Generator

One click. One random dad joke. Zero apologies. Works at parties, in the car line, and mid-argument.

Click the button to summon a dad joke.

How to weaponize a dad joke

  1. Deliver with confidence. Eye contact is everything. The dad joke fears hesitation.
  2. Pause before the punchline. Let the suspense marinate. Then pounce.
  3. Ignore the groans. The groan is applause in dad dialect.
  4. Repeat the best ones. Repetition builds tradition. Tradition builds groans.

Looking for something specific? Search all dad jokes or browse by category.

""" 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"""

๐Ÿ˜„ Dad Joke of the Day

A brand-new dad joke every single day. Doctors recommend exactly one per day. We won't tell if you don't.

{d.strftime('%B %d, %Y')}

{esc(j['setup'])}

{esc(j['punch'])}

The dad joke archive

Every recent dad joke of the day, one per day, no repeats (for a few months, anyway):

""" 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/', '' % SITE_URL + '' % esc(json.dumps(ld))) def page_search(): body = """

๐Ÿ”Ž Search Dad Jokes

Looking for a dad joke about coffee, cats, or cardio? Type it in. We probably have a pun for that.

No jokes matched. Which, honestly, is a great setup for a new dad joke โ€” send it to us via the contact page.

""" 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 = """

๐Ÿ‘‹ About Best Dad Jokes

Welcome to BestDadJokes.lol โ€” 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.

The mission is simple: build the most useful dad joke resource on the internet. That means:

Why dad jokes?

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.

Get in touch

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 contact page.

""" 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 = """

๐Ÿ“ฎ Contact

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.

Email us at hello@bestdadjokes.lol โ€” responses may be delayed by naps.

Joke submissions

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.

""" 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 = """

Privacy Policy

Last updated: %s

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:

What we collect

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.

Cookies and advertising

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.

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 Google Ads Settings, or opt out of third-party vendor cookies at aboutads.info.

If we run a consent banner for visitors from the EEA/UK, your choices will be honored and stored locally.

Analytics

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.

Data sharing

We do not sell personal data. We have no personal data to sell. We barely have data.

Children

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.

Contact

Questions about this policy? Email hello@bestdadjokes.lol.

""" % 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 = """

Terms of Use

Last updated: %s

By using BestDadJokes.lol you agree to the following terms. Reading them aloud in a dad voice is optional but encouraged.

Use of content

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.

No warranties

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.

Limitation of liability

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.

External links

We are not responsible for the content of external websites, including their jokes, which are almost certainly worse than ours.

Changes

We may update these terms occasionally. Continued use of the site means you accept the updated terms, and possibly a fresh dad joke.

""" % 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 = """

404

This page is like a dad joke that got cut from the site โ€” it just didn't land.

Why did the web page go to therapy?

It had too many broken links.

Take me home โ†’

""" 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 = ['', ''] for p in pages: sm.append('%s%s%s%s%s' % (SITE_URL, p, today, 'weekly' if p == '/' else 'monthly', '1.0' if p == '/' else '0.8')) sm.append('') 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( '%s%s/joke-of-the-day/?d=%s' 'bjd-daily-%s' '%s%s โ€” %s' % (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 = ('' '%s โ€” Dad Joke of the Day' '%s/joke-of-the-day/%s' 'en-us%s' % (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()