From 6483300e1c0ff3207448682301eef2ced31118ae Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Mon, 23 Mar 2026 22:31:52 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20add=20SEO=20foundation=20=E2=80=94=20me?= =?UTF-8?q?ta=20tags,=20sitemap,=20robots.txt,=20JSON-LD=20(#813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `site_url` setting to config (default: https://alexanderwhitestone.com, override with SITE_URL env var) - Inject `site_url` as a Jinja2 global in templating.py so all templates can reference it without per-route boilerplate - Update base.html to "Timmy AI Workshop | Lightning-Powered AI Jobs — Pay Per Task with Bitcoin" with per-page override block - Add SEO meta blocks to base.html: - `{% block meta_description %}` with Lightning/sats copy - `{% block meta_robots %}` defaulting to "index, follow" - `{% block canonical_url %}` link tag pointing to site_url - Open Graph tags (og:title, og:description, og:url, og:image) - Twitter/X Card summary_large_image tags - JSON-LD structured data: SoftwareApplication, Service (paymentAccepted: Bitcoin Lightning), Organization, FAQPage - Add src/dashboard/routes/seo.py with GET /robots.txt and GET /sitemap.xml (lists 14 crawlable pages with changefreq + priority) - Register seo_router before all other routers in app.py Fixes #813 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/config.py | 5 ++ src/dashboard/app.py | 2 + src/dashboard/routes/seo.py | 73 +++++++++++++++++++++++ src/dashboard/templates/base.html | 98 ++++++++++++++++++++++++++++++- src/dashboard/templating.py | 5 ++ 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/dashboard/routes/seo.py diff --git a/src/config.py b/src/config.py index bb72f70f..c257bb8d 100644 --- a/src/config.py +++ b/src/config.py @@ -571,6 +571,11 @@ class Settings(BaseSettings): content_meilisearch_url: str = "http://localhost:7700" content_meilisearch_api_key: str = "" + # ── SEO / Public Site ────────────────────────────────────────────────── + # Canonical base URL used in sitemap.xml, canonical link tags, and OG tags. + # Override with SITE_URL env var, e.g. "https://alexanderwhitestone.com". + site_url: str = "https://alexanderwhitestone.com" + # ── Scripture / Biblical Integration ────────────────────────────── # Enable the biblical text module. scripture_enabled: bool = True diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 25252942..ffa07e4c 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -62,6 +62,7 @@ from dashboard.routes.tools import router as tools_router from dashboard.routes.tower import router as tower_router from dashboard.routes.voice import router as voice_router from dashboard.routes.work_orders import router as work_orders_router +from dashboard.routes.seo import router as seo_router from dashboard.routes.world import matrix_router from dashboard.routes.world import router as world_router from timmy.workshop_state import PRESENCE_FILE @@ -663,6 +664,7 @@ if static_dir.exists(): from dashboard.templating import templates # noqa: E402 # Include routers +app.include_router(seo_router) app.include_router(health_router) app.include_router(agents_router) app.include_router(voice_router) diff --git a/src/dashboard/routes/seo.py b/src/dashboard/routes/seo.py new file mode 100644 index 00000000..b943870f --- /dev/null +++ b/src/dashboard/routes/seo.py @@ -0,0 +1,73 @@ +"""SEO endpoints: robots.txt, sitemap.xml, and structured-data helpers. + +These endpoints make alexanderwhitestone.com crawlable by search engines. +All pages listed in the sitemap are server-rendered HTML (not SPA-only). +""" + +from __future__ import annotations + +from datetime import date + +from fastapi import APIRouter +from fastapi.responses import PlainTextResponse, Response + +from config import settings + +router = APIRouter(tags=["seo"]) + +# Public-facing pages included in the sitemap. +# Format: (path, change_freq, priority) +_SITEMAP_PAGES: list[tuple[str, str, str]] = [ + ("/", "daily", "1.0"), + ("/briefing", "daily", "0.9"), + ("/tasks", "daily", "0.8"), + ("/calm", "weekly", "0.7"), + ("/thinking", "weekly", "0.7"), + ("/swarm/mission-control", "weekly", "0.7"), + ("/monitoring", "weekly", "0.6"), + ("/nexus", "weekly", "0.6"), + ("/spark/ui", "weekly", "0.6"), + ("/memory", "weekly", "0.6"), + ("/marketplace/ui", "weekly", "0.8"), + ("/models", "weekly", "0.5"), + ("/tools", "weekly", "0.5"), + ("/scorecards", "weekly", "0.6"), +] + + +@router.get("/robots.txt", response_class=PlainTextResponse) +async def robots_txt() -> str: + """Allow all search engines; point to sitemap.""" + base = settings.site_url.rstrip("/") + return ( + "User-agent: *\n" + "Allow: /\n" + "\n" + f"Sitemap: {base}/sitemap.xml\n" + ) + + +@router.get("/sitemap.xml") +async def sitemap_xml() -> Response: + """Generate XML sitemap for all crawlable pages.""" + base = settings.site_url.rstrip("/") + today = date.today().isoformat() + + url_entries: list[str] = [] + for path, changefreq, priority in _SITEMAP_PAGES: + url_entries.append( + f" <url>\n" + f" <loc>{base}{path}</loc>\n" + f" <lastmod>{today}</lastmod>\n" + f" <changefreq>{changefreq}</changefreq>\n" + f" <priority>{priority}</priority>\n" + f" </url>" + ) + + xml = ( + '<?xml version="1.0" encoding="UTF-8"?>\n' + '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + + "\n".join(url_entries) + + "\n</urlset>\n" + ) + return Response(content=xml, media_type="application/xml") diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index d20b1fe0..4ce667c0 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -6,7 +6,103 @@ <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="theme-color" content="#080412" /> - <title>{% block title %}Timmy Time — Mission Control{% endblock %} + {% block title %}Timmy AI Workshop | Lightning-Powered AI Jobs — Pay Per Task with Bitcoin{% endblock %} + + {# SEO: description #} + + + + {# Canonical URL — override per-page via {% block canonical_url %} #} + {% block canonical_url %} + + {% endblock %} + + {# Open Graph #} + + + + + + + + + {# Twitter / X Card #} + + + + + + {# JSON-LD Structured Data #} + + diff --git a/src/dashboard/templating.py b/src/dashboard/templating.py index 46d60527..aea02947 100644 --- a/src/dashboard/templating.py +++ b/src/dashboard/templating.py @@ -4,4 +4,9 @@ from pathlib import Path from fastapi.templating import Jinja2Templates +from config import settings + templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) + +# Inject site_url into every template so SEO tags and canonical URLs work. +templates.env.globals["site_url"] = settings.site_url -- 2.43.0