From 09c1c841531a72fc53ff38210b2c96c2f50a3cd8 Mon Sep 17 00:00:00 2001 From: grabowski Date: Tue, 11 Aug 2026 16:33:44 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20search-engine=20indexing=20=E2=80=94=20?= =?UTF-8?q?robots.txt,=20sitemap.xml,=20llms.txt,=20SEO=20meta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit robots.txt (sitemap pointer, /metrics and /scrape/ disallowed), sitemap.xml (/ and /docs), and llms.txt (site + API summary for LLM crawlers) served at the domain root. Dashboard head gains title with keywords, meta description, canonical, Open Graph/Twitter tags, schema.org WebApplication JSON-LD, theme-color, and an inline SVG favicon (also silences the /favicon.ico 404). --- src/static/dashboard.html | 13 ++++++++++++- src/static/llms.txt | 34 ++++++++++++++++++++++++++++++++++ src/static/robots.txt | 6 ++++++ src/static/sitemap.xml | 13 +++++++++++++ src/web_api.py | 23 ++++++++++++++++++++++- tests/test_dashboard.py | 20 ++++++++++++++++++++ 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/static/llms.txt create mode 100644 src/static/robots.txt create mode 100644 src/static/sitemap.xml diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 5c25717..95e4110 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -3,7 +3,18 @@ - Ping River Live Monitor + Ping River Live Monitor · Chiang Mai Water Levels & Flood Forecast + + + + + + + + + + + diff --git a/src/static/llms.txt b/src/static/llms.txt new file mode 100644 index 0000000..20124a4 --- /dev/null +++ b/src/static/llms.txt @@ -0,0 +1,34 @@ +# Ping River Live Monitor + +> Live water levels, discharge, rainfall and machine-learning flood forecasts for +> the Ping River basin in Northern Thailand, centered on Chiang Mai (gauge P.1, +> Nawarat Bridge). Open-source, hourly data since 2018, operated by B4L +> (https://buildfor.life). + +Flood context: Chiang Mai city flooding begins when P.1 reaches 3.70 m +(official inundation stage 1); the record peak was 5.30 m on 2024-10-05. + +## Data + +- Hourly water level (m) and discharge (m³/s) from 16 Royal Irrigation + Department (RID) telemetry gauges in the Ping basin, 2018-present. +- Hourly rainfall and water level for 400+ ThaiWater/HII stations (Ping basin), + including historical water levels back to 2019. +- ML flood-risk forecasts (probability of warning/danger level within 6/12/24 h) + per station, plus Chiang Mai inundation-stage probabilities. + +## API (no key required) + +- https://water.buildfor.life/docs : interactive OpenAPI reference +- /stations : station metadata (code, names, coordinates) +- /measurements/latest?limit=N : latest readings +- /measurements/history/{station_code}?hours=N or ?start=YYYY-MM-DD&end=YYYY-MM-DD : history +- /forecast : flood-risk forecasts for all stations +- /api/stats : database coverage statistics +- /api/hii/rainfall/latest : latest rainfall per ThaiWater/HII station +- /api/hii/waterlevel/latest : latest water level per ThaiWater/HII station (m MSL) + +## Source + +- https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor : code, docs, + and the full data-source catalog (docs/DATA_SOURCES.md). diff --git a/src/static/robots.txt b/src/static/robots.txt new file mode 100644 index 0000000..f3abfad --- /dev/null +++ b/src/static/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /metrics +Disallow: /scrape/ + +Sitemap: https://water.buildfor.life/sitemap.xml diff --git a/src/static/sitemap.xml b/src/static/sitemap.xml new file mode 100644 index 0000000..6f5cbf5 --- /dev/null +++ b/src/static/sitemap.xml @@ -0,0 +1,13 @@ + + + + https://water.buildfor.life/ + hourly + 1.0 + + + https://water.buildfor.life/docs + monthly + 0.5 + + diff --git a/src/web_api.py b/src/web_api.py index 0d32a0e..6c5b5e6 100644 --- a/src/web_api.py +++ b/src/web_api.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional import requests from fastapi import BackgroundTasks, Depends, FastAPI, Header, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from .config import Config @@ -276,6 +276,27 @@ async def root(): return HTMLResponse(content=DASHBOARD_HTML) +# Crawler/indexing files must live at the domain root (served from src/static) +_STATIC_DIR = os.path.dirname(_DASHBOARD_HTML_PATH) + + +@app.get("/robots.txt", include_in_schema=False) +async def robots_txt(): + return FileResponse(os.path.join(_STATIC_DIR, "robots.txt"), media_type="text/plain") + + +@app.get("/llms.txt", include_in_schema=False) +async def llms_txt(): + return FileResponse(os.path.join(_STATIC_DIR, "llms.txt"), media_type="text/plain") + + +@app.get("/sitemap.xml", include_in_schema=False) +async def sitemap_xml(): + return FileResponse( + os.path.join(_STATIC_DIR, "sitemap.xml"), media_type="application/xml" + ) + + @app.get("/health", response_model=HealthResponse) async def get_health(): """Get system health status""" diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 7896729..f9732f8 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -40,6 +40,26 @@ def test_dashboard_loads_additional_thaiwater_sensors(): assert "applyStationSearch" in html +def test_dashboard_has_seo_and_indexing_files(): + html = DASHBOARD_PATH.read_text(encoding="utf-8") + static_dir = DASHBOARD_PATH.parent + + assert '' in html + assert 'property="og:title"' in html + assert 'rel="icon"' in html + + robots = (static_dir / "robots.txt").read_text(encoding="utf-8") + assert "Sitemap: https://water.buildfor.life/sitemap.xml" in robots + assert "https://water.buildfor.life/" in (static_dir / "sitemap.xml").read_text(encoding="utf-8") + assert "Ping River Live Monitor" in (static_dir / "llms.txt").read_text(encoding="utf-8") + + from src import web_api + + routes = {route.path for route in web_api.app.routes} + assert {"/robots.txt", "/llms.txt", "/sitemap.xml"} <= routes + + def test_dashboard_shows_hii_rainfall_layer(): html = DASHBOARD_PATH.read_text(encoding="utf-8")