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
|
||||
def inject_globals() -> dict:
|
||||
base_url = app.config.get("APP_BASE_URL", "https://myboker.org").rstrip("/")
|
||||
return {
|
||||
"app_version": app.config["APP_VERSION"],
|
||||
"current_user_id": current_user_id(),
|
||||
"is_logged_in": is_logged_in(),
|
||||
"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)
|
||||
|
||||
+68
-2
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask import current_app
|
||||
from datetime import date
|
||||
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.config import ELIGIBLE_MIN_SESSIONS
|
||||
@@ -18,11 +21,74 @@ from boker.utils import session_label, session_sort_key
|
||||
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("/")
|
||||
def home():
|
||||
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")
|
||||
def help():
|
||||
return render_template("support.html")
|
||||
|
||||
+14
-1
@@ -1,12 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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 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="stylesheet" href="{{ url_for('static', filename='css/tokens.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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% 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 %}
|
||||
|
||||
<div class="explore-header">
|
||||
|
||||
+29
-1
@@ -1,5 +1,33 @@
|
||||
{% 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 content %}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% 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 content %}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% 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 %}
|
||||
|
||||
<div class="hero" style="padding-bottom:12px;">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% 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 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