This commit is contained in:
SowinskiBraeden committed 2026-06-29 11:58:28 -07:00
1 parent 72791a6258
commit 74d8baa6d7
9 files changed
+189 -6

No files matched your search

+7
View File
@@ -51,11 +51,18 @@ def create_app(config_overrides: dict | None = None) -> Flask:
@app.context_processor @app.context_processor
def inject_globals() -> dict: def inject_globals() -> dict:
base_url = app.config.get("APP_BASE_URL", "https://myboker.org").rstrip("/")
return { return {
"app_version": app.config["APP_VERSION"], "app_version": app.config["APP_VERSION"],
"current_user_id": current_user_id(), "current_user_id": current_user_id(),
"is_logged_in": is_logged_in(), "is_logged_in": is_logged_in(),
"current_user_is_site_admin": is_site_admin(), "current_user_is_site_admin": is_site_admin(),
"seo_site_url": base_url,
"seo_canonical_url": f"{base_url}{request.path}",
"seo_default_description": (
"Free home poker league tracker for sessions, ledgers, leaderboards, "
"settlement, buy-ins, cashouts, and private game records."
),
} }
app.register_blueprint(public_bp) app.register_blueprint(public_bp)
+68 -2
View File
@@ -1,8 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from flask import Blueprint, flash, redirect, render_template, request, url_for from datetime import date
from flask import current_app from xml.sax.saxutils import escape as xml_escape
from flask import Blueprint, Response, current_app, flash, redirect, render_template, request, url_for
from sqlalchemy.exc import SQLAlchemyError
from boker.charts import cumulative_profit_series, player_session_series, session_breakdown_series from boker.charts import cumulative_profit_series, player_session_series, session_breakdown_series
from boker.config import ELIGIBLE_MIN_SESSIONS from boker.config import ELIGIBLE_MIN_SESSIONS
@@ -18,11 +21,74 @@ from boker.utils import session_label, session_sort_key
public_bp = Blueprint("public", __name__) public_bp = Blueprint("public", __name__)
def _absolute_url(path: str) -> str:
base_url = current_app.config.get("APP_BASE_URL", "https://myboker.org").rstrip("/")
return f"{base_url}{path}"
def _sitemap_url(path: str, priority: str, changefreq: str = "weekly") -> str:
return (
" <url>\n"
f" <loc>{xml_escape(_absolute_url(path))}</loc>\n"
f" <lastmod>{date.today().isoformat()}</lastmod>\n"
f" <changefreq>{changefreq}</changefreq>\n"
f" <priority>{priority}</priority>\n"
" </url>"
)
@public_bp.get("/") @public_bp.get("/")
def home(): def home():
return render_template("landing.html") return render_template("landing.html")
@public_bp.get("/robots.txt")
def robots_txt():
body = "\n".join(
[
"User-agent: *",
"Allow: /",
"Disallow: /internal/",
"Disallow: /account/",
"Sitemap: " + _absolute_url(url_for("public.sitemap_xml")),
"",
]
)
return Response(body, mimetype="text/plain")
@public_bp.get("/sitemap.xml")
def sitemap_xml():
urls = [
_sitemap_url(url_for("public.home"), "1.0", "weekly"),
_sitemap_url(url_for("public.explore"), "0.8", "daily"),
_sitemap_url(url_for("public.help"), "0.7", "monthly"),
_sitemap_url(url_for("public.privacy"), "0.3", "yearly"),
_sitemap_url(url_for("public.terms"), "0.3", "yearly"),
]
from boker.db import database_extensions_available
if database_extensions_available():
try:
from boker.league_repositories import list_public_leagues
for league in list_public_leagues():
urls.append(_sitemap_url(url_for("leagues.dashboard", league_ref=league.url_ref), "0.6", "weekly"))
urls.append(_sitemap_url(url_for("leagues.leaderboard", league_ref=league.url_ref), "0.5", "weekly"))
urls.append(_sitemap_url(url_for("leagues.sessions", league_ref=league.url_ref), "0.5", "weekly"))
except SQLAlchemyError:
current_app.logger.warning("Sitemap public league query failed", exc_info=True)
body = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
+ "\n".join(urls)
+ "\n</urlset>\n"
)
return Response(body, mimetype="application/xml")
@public_bp.get("/help") @public_bp.get("/help")
def help(): def help():
return render_template("support.html") return render_template("support.html")
+14 -1
View File
@@ -1,12 +1,25 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
{% set page_title %}{% block title %}myboker.org{% endblock %}{% endset %}
{% set page_description %}{% block meta_description %}{{ seo_default_description }}{% endblock %}{% endset %}
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}myboker.org{% endblock %}</title> <title>{{ page_title | trim }}</title>
<meta name="description" content="{{ page_description | striptags | trim }}">
<link rel="canonical" href="{{ seo_canonical_url }}">
<meta property="og:site_name" content="myboker.org">
<meta property="og:title" content="{{ page_title | trim }}">
<meta property="og:description" content="{{ page_description | striptags | trim }}">
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
<meta property="og:url" content="{{ seo_canonical_url }}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ page_title | trim }}">
<meta name="twitter:description" content="{{ page_description | striptags | trim }}">
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml"> <link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ url_for('static', filename='css/tokens.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/tokens.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block structured_data %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head> </head>
<body> <body>
+2 -1
View File
@@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Explore leagues · myboker.org{% endblock %} {% block title %}Explore Public Home Poker Leagues · myboker.org{% endblock %}
{% block meta_description %}Browse public home poker league results, leaderboards, sessions, and player stats from groups using myboker.org.{% endblock %}
{% block content %} {% block content %}
<div class="explore-header"> <div class="explore-header">
+29 -1
View File
@@ -1,5 +1,33 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}myboker.org · Poker league records{% endblock %} {% block title %}Free Home Poker Tracker, Ledger & League Leaderboards · myboker.org{% endblock %}
{% block meta_description %}Track home poker sessions, buy-ins, cashouts, settlements, player stats, and league leaderboards with a free append-only poker ledger built for private games.{% endblock %}
{% block structured_data %}
<script type="application/ld+json">
{{ {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "myboker.org",
"url": seo_site_url,
"applicationCategory": "FinanceApplication",
"operatingSystem": "Web",
"description": "Free home poker league tracker for sessions, ledgers, leaderboards, buy-ins, cashouts, and settlement records.",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
} | tojson }}
</script>
<script type="application/ld+json">
{{ {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "myboker.org",
"url": seo_site_url,
"description": "Home poker tracker for private game ledgers, sessions, settlement, and league leaderboards."
} | tojson }}
</script>
{% endblock %}
{% block page_class %}page--landing{% endblock %} {% block page_class %}page--landing{% endblock %}
{% block content %} {% block content %}
+1
View File
@@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Privacy Policy - myboker.org{% endblock %} {% block title %}Privacy Policy - myboker.org{% endblock %}
{% block meta_description %}Privacy policy for myboker.org, including account data, home poker league records, session ledgers, cookies, security, and data rights.{% endblock %}
{% block page_class %}page--public{% endblock %} {% block page_class %}page--public{% endblock %}
{% block content %} {% block content %}
+2 -1
View File
@@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Help - myboker.org{% endblock %} {% block title %}Home Poker League Tracker Help · myboker.org{% endblock %}
{% block meta_description %}Learn how myboker.org tracks home poker leagues, sessions, players, ledger events, settlements, leaderboards, roles, and public or private league visibility.{% endblock %}
{% block content %} {% block content %}
<div class="hero" style="padding-bottom:12px;"> <div class="hero" style="padding-bottom:12px;">
+1
View File
@@ -1,5 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Terms of Service - myboker.org{% endblock %} {% block title %}Terms of Service - myboker.org{% endblock %}
{% block meta_description %}Terms of Service for myboker.org, a free record-keeping tool for private home poker leagues, ledgers, sessions, and statistics.{% endblock %}
{% block page_class %}page--public{% endblock %} {% block page_class %}page--public{% endblock %}
{% block content %} {% block content %}
+65
View File
@@ -0,0 +1,65 @@
import tempfile
import unittest
from pathlib import Path
from app import create_app
from boker.db import db
class SeoTests(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
db_path = Path(self.tmpdir.name) / "test.sqlite3"
self.app = create_app(
{
"TESTING": True,
"APP_BASE_URL": "https://myboker.org",
"SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}",
}
)
self.client = self.app.test_client()
with self.app.app_context():
db.create_all()
def tearDown(self):
with self.app.app_context():
db.session.remove()
db.drop_all()
db.engine.dispose()
self.tmpdir.cleanup()
def test_landing_page_has_search_metadata(self):
response = self.client.get("/")
html = response.get_data(as_text=True)
self.assertEqual(response.status_code, 200)
self.assertIn("Free Home Poker Tracker, Ledger & League Leaderboards", html)
self.assertIn('name="description"', html)
self.assertIn("Track home poker sessions, buy-ins, cashouts, settlements", html)
self.assertIn('rel="canonical" href="https://myboker.org/"', html)
self.assertIn('application/ld+json', html)
def test_robots_txt_allows_public_crawling_and_points_to_sitemap(self):
response = self.client.get("/robots.txt")
body = response.get_data(as_text=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "text/plain")
self.assertIn("Allow: /", body)
self.assertIn("Disallow: /internal/", body)
self.assertIn("Disallow: /account/", body)
self.assertIn("Sitemap: https://myboker.org/sitemap.xml", body)
def test_sitemap_xml_lists_public_static_pages(self):
response = self.client.get("/sitemap.xml")
body = response.get_data(as_text=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.mimetype, "application/xml")
self.assertIn("<loc>https://myboker.org/</loc>", body)
self.assertIn("<loc>https://myboker.org/explore</loc>", body)
self.assertIn("<loc>https://myboker.org/help</loc>", body)
if __name__ == "__main__":
unittest.main()