seo
This commit is contained in:
9 files changed
+189
-6
No files matched your search
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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,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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in new issue
Block a user