Add OG social cards, Mermaid diagrams, and robots.txt

- OG images: auto-generated per page via astro-og-canvas with maritime
  teal theme, wired through Starlight route data middleware
- Mermaid: request flow and parallel fetch diagrams on architecture page
- robots.txt: sitemap and llms.txt references for crawlers
This commit is contained in:
Ryan Malloy 2026-02-24 13:33:34 -07:00
parent e519c100ed
commit a5009941ab
7 changed files with 1444 additions and 3 deletions

View File

@ -1,4 +1,5 @@
import { defineConfig } from "astro/config";
import mermaid from "astro-mermaid";
import starlight from "@astrojs/starlight";
import starlightLlmsTxt from "starlight-llms-txt";
@ -10,10 +11,12 @@ export default defineConfig({
telemetry: false,
devToolbar: { enabled: false },
integrations: [
mermaid(),
starlight({
title: "mcnoaa-tides",
description:
"FastMCP server for NOAA CO-OPS tide predictions, water levels, and marine conditions.",
routeMiddleware: "./src/routeData.ts",
plugins: [
starlightLlmsTxt({
projectName: "mcnoaa-tides",

1314
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,8 @@
"@iconify-json/lucide": "^1.2.91",
"astro": "^5.6.1",
"astro-icon": "^1.1.5",
"astro-mermaid": "^1.3.1",
"astro-og-canvas": "^0.10.1",
"sharp": "^0.34.2",
"starlight-llms-txt": "^0.7.0"
}

7
docs/public/robots.txt Normal file
View File

@ -0,0 +1,7 @@
User-agent: *
Allow: /
Sitemap: https://mcnoaa-tides.warehack.ing/sitemap-index.xml
# LLM documentation — https://llmstxt.org/
LLMs-Txt: https://mcnoaa-tides.warehack.ing/llms.txt

View File

@ -9,6 +9,35 @@ import { Aside, Card, CardGrid, Steps, FileTree, Badge } from "@astrojs/starligh
mcnoaa-tides is a FastMCP server that wraps the NOAA CO-OPS Tides and Currents API. This page describes how the pieces fit together -- the server lifecycle, caching strategy, parallel fetch patterns, and module organization.
## Request Flow
```mermaid
flowchart LR
User([User])
LLM[LLM Client]
MCP[MCP Transport<br/>stdio / HTTP]
Server[mcnoaa-tides<br/>FastMCP Server]
Cache[(Station<br/>Cache)]
DataAPI[NOAA Data API<br/>Predictions &<br/>Observations]
MetaAPI[NOAA Metadata API<br/>Station Catalog]
User -->|natural language| LLM
LLM -->|tool call| MCP
MCP --> Server
Server -->|cache hit| Cache
Server -->|parallel fetch| DataAPI
Server -->|catalog refresh| MetaAPI
Cache -.->|24h TTL| MetaAPI
style User fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style LLM fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style MCP fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style Server fill:#1a8a8f,stroke:#5ec4c8,color:#0a1517
style Cache fill:#0d3b3e,stroke:#5ec4c8,color:#e8f0f0
style DataAPI fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style MetaAPI fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
```
## Server Lifecycle
The server uses FastMCP's lifespan context manager to own the `NOAAClient` instance. Every tool call receives the same client through the lifespan context, so there is exactly one HTTP connection pool and one station cache for the entire server process.
@ -86,9 +115,28 @@ Several tools fire multiple API calls simultaneously using `asyncio.gather`. Thi
Fetches 6 products in parallel:
```
predictions (hilo) | water_level | water_temperature
air_temperature | wind | air_pressure
```mermaid
flowchart LR
Tool[marine_conditions<br/>_snapshot]
P[predictions<br/>hilo]
WL[water_level]
WT[water_temperature]
AT[air_temperature]
W[wind]
AP[air_pressure]
R[Combined<br/>Response]
Tool --> P & WL & WT & AT & W & AP
P & WL & WT & AT & W & AP --> R
style Tool fill:#1a8a8f,stroke:#5ec4c8,color:#0a1517
style R fill:#1a8a8f,stroke:#5ec4c8,color:#0a1517
style P fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style WL fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style WT fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style AT fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style W fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
style AP fill:#0d3b3e,stroke:#1a8a8f,color:#e8f0f0
```
Each product is fetched independently. If a product fails (sensor not available at the station, temporary API error), it is recorded under an `"unavailable"` key in the response rather than failing the entire request.

View File

@ -0,0 +1,40 @@
import { getCollection } from "astro:content";
import { OGImageRoute } from "astro-og-canvas";
const entries = await getCollection("docs");
const pages = Object.fromEntries(
entries.map(({ data, id }) => [id, { data }]),
);
export const { getStaticPaths, GET } = await OGImageRoute({
pages,
param: "slug",
getImageOptions: (_id, page: (typeof pages)[number]) => ({
title: page.data.title,
description: page.data.description,
bgGradient: [[10, 21, 23]],
border: { color: [26, 138, 143], width: 20 },
padding: 120,
font: {
title: {
size: 64,
weight: "Bold",
color: [232, 240, 240],
families: ["Inter"],
},
description: {
size: 28,
color: [196, 212, 214],
families: ["Inter"],
},
},
fonts: [
"https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.woff2",
"https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.woff2",
],
logo: {
path: "./public/favicon.svg",
size: [60],
},
}),
});

27
docs/src/routeData.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineRouteMiddleware } from "@astrojs/starlight/route-data";
export const onRequest = defineRouteMiddleware((context) => {
const ogImageUrl = new URL(
`/og/${context.locals.starlightRoute.id || "index"}.png`,
context.site,
);
const { head } = context.locals.starlightRoute;
head.push({
tag: "meta",
attrs: { property: "og:image", content: ogImageUrl.href },
});
head.push({
tag: "meta",
attrs: { property: "og:image:width", content: "1200" },
});
head.push({
tag: "meta",
attrs: { property: "og:image:height", content: "630" },
});
head.push({
tag: "meta",
attrs: { name: "twitter:image", content: ogImageUrl.href },
});
});