Files
qtalker---/backend/app/routes/seo.py
Indiana 7906abdd2b feat: /secret — the grimoire
An unlisted page at /secret. Nothing in the app links to it, it has no nav
tab, and MobileNav's route-parity test now names it as an explicit exception
so an ordinary page still fails that test. It IS crawlable and is advertised
in the sitemap: "hidden" here means no button, not no index — a long,
specific mechanics page is worth the search traffic.

Every number on it was read out of the source or produced by RUNNING the
real function. A grimoire of plausible-sounding numbers would be worse than
none, because it would be believed. Four hand-rolled SVG figures (rarity by
moon phase, the passage ladder, the two twist curves) rather than a charting
dependency for four static pictures.

The best thing in it is a trap I did not know existed until I ran the code:
crossing over requires volatility strictly above 0.6, and favor shifts
volatility DOWN by up to 0.12 at mint. So a hunter at maximum favor pulls
spirits under the gate and locks themselves out of the largest reward in the
game — 25 essence and +0.08 favor. Verified directly: a spirit at volatility
0.65 is crossable at favor 0.0, and is not at +0.5 or +1.0. "Do not maximise
favor" is real, counter-intuitive, and follows from the source.

English only, as agreed — written as prose outside the i18n system rather
than several hundred translation keys, which keeps the en/es parity gate
meaningful for the actual UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:14:59 +00:00

113 lines
4.0 KiB
Python

"""robots.txt and a live sitemap.
Served from the backend rather than dropped in `public/` because the
valuable, indexable surface of this site is the Codex, and the Codex grows
every time somebody summons. A static sitemap would be stale within an
hour; this one is generated from the actual entity rows.
What is deliberately NOT listed: /seance, /enter, /profile, /log,
/inventory, /devices — anything per-seeker or interactive. A crawler
hitting /seance would provision a guest account on arrival (the open
door), which would fill the users table with wanderers that never
existed as people. robots.txt disallows those paths for the same reason,
and the sitemap only advertises pages that are genuinely public,
stable, and worth a search result: the landing page, the shop, and every
discovered spirit.
"""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.db import get_db
from app.models.entity import Entity
router = APIRouter(tags=["seo"])
# Cap the sitemap so a runaway Codex can't produce a multi-megabyte
# document. 5k is far inside the 50k/50MB sitemap limit and orders of
# magnitude beyond what this install will realistically hold.
MAX_SITEMAP_ENTITIES = 5000
# Paths that must never be crawled: they either mutate state (provisioning a
# guest) or are meaningless without a session.
DISALLOWED = (
"/seance",
"/enter",
"/profile",
"/hunters",
"/log",
"/inventory",
"/devices",
"/api/",
)
def _base_url() -> str:
return settings.public_base_url.rstrip("/")
def _iso(dt: datetime | None) -> str:
value = dt or datetime.now(timezone.utc)
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.date().isoformat()
def _xml_escape(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
@router.get("/robots.txt", include_in_schema=False)
async def robots() -> Response:
lines = ["User-agent: *"]
lines += [f"Disallow: {path}" for path in DISALLOWED]
lines.append("Allow: /")
lines.append(f"Sitemap: {_base_url()}/sitemap.xml")
return Response("\n".join(lines) + "\n", media_type="text/plain")
@router.get("/sitemap.xml", include_in_schema=False)
async def sitemap(db: AsyncSession = Depends(get_db)) -> Response:
base = _base_url()
result = await db.execute(
select(Entity.id, Entity.discovered_at)
.order_by(Entity.discovered_at.desc())
.limit(MAX_SITEMAP_ENTITIES)
)
entities = result.all()
parts = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
f"<url><loc>{base}/</loc><changefreq>daily</changefreq>"
"<priority>1.0</priority></url>",
f"<url><loc>{base}/codex</loc><changefreq>hourly</changefreq>"
"<priority>0.9</priority></url>",
f"<url><loc>{base}/shop</loc><changefreq>weekly</changefreq>"
"<priority>0.7</priority></url>",
# /secret is UNLISTED, not private: nothing in the app links to it and
# it has no nav tab, so a seeker only finds it by being told. It is
# advertised here anyway because it is a long, specific mechanics page
# and search traffic is the whole reason it is crawlable — "hidden"
# here means "no button", not "no index".
f"<url><loc>{base}/secret</loc><changefreq>monthly</changefreq>"
"<priority>0.8</priority></url>",
]
for entity_id, discovered_at in entities:
loc = _xml_escape(f"{base}/codex/{entity_id}")
parts.append(
f"<url><loc>{loc}</loc><lastmod>{_iso(discovered_at)}</lastmod>"
"<changefreq>weekly</changefreq><priority>0.6</priority></url>"
)
parts.append("</urlset>")
return Response("\n".join(parts), media_type="application/xml")