Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions fintick/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,16 @@ def read_feed(database: str | Path, *, limit: int = DEFAULT_LIMIT) -> dict[str,

SITE_ORIGIN = os.environ.get("FINTICK_SITE_ORIGIN", "https://fintick.fyi").rstrip("/")

# Crawlable: the board itself. Not crawlable: the JSON API (no prose to index) and the
# ?ops operator view, which is the same page plus telemetry and would read as duplicate
# content. The canonical link handles ?ops for engines that ignore the query rule.
# Crawlable: the board itself. Not crawlable: the JSON API, which carries no prose to
# index.
#
# The operator view is deliberately NOT named here. robots.txt is a public file that
# scanners fetch first, so a Disallow line advertises a path rather than protecting it.
# Consolidating the operator view onto the board is the canonical link's job, and it
# does that without publishing anything.
ROBOTS_TXT = """User-agent: *
Allow: /$
Disallow: /api/
Disallow: /*?ops

Sitemap: {origin}/sitemap.xml
"""
Expand Down
10 changes: 8 additions & 2 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,21 @@ def test_structured_data_parses(self) -> None:
graph = json.loads(block)["@graph"]
self.assertEqual({node["@type"] for node in graph}, {"WebSite", "WebApplication"})

def test_robots_points_at_sitemap_and_shields_api_and_ops(self) -> None:
def test_robots_points_at_sitemap_and_disallows_the_api(self) -> None:
status, headers, body = self._get("/robots.txt")
text = body.decode()
self.assertEqual(status, 200)
self.assertTrue(headers["Content-Type"].startswith("text/plain"))
self.assertIn("Disallow: /api/", text)
self.assertIn("Disallow: /*?ops", text)
self.assertIn("Sitemap: https://fintick.fyi/sitemap.xml", text)

def test_public_documents_never_name_the_operator_view(self) -> None:
# robots.txt and llms.txt are fetched by scanners. Naming the operator view in
# either advertises it; a Disallow line is a signpost, not a shield.
for path in ("/robots.txt", "/llms.txt", "/sitemap.xml"):
_, _, body = self._get(path)
self.assertNotIn("ops", body.decode().lower(), f"{path} names the operator view")

def test_sitemap_is_well_formed_xml(self) -> None:
import xml.etree.ElementTree as ET

Expand Down