diff --git a/apps/developer-hub/.prettierignore b/apps/developer-hub/.prettierignore index 2d76e1aff0..f581727d6f 100644 --- a/apps/developer-hub/.prettierignore +++ b/apps/developer-hub/.prettierignore @@ -11,4 +11,5 @@ build/ node_modules/ package.json tsconfig*.json -content/docs/price-feeds/core/use-real-time-data/pull-integration/ton.mdx \ No newline at end of file +content/docs/price-feeds/core/use-real-time-data/pull-integration/ton.mdx +content/docs/api-reference/** \ No newline at end of file diff --git a/apps/developer-hub/README.md b/apps/developer-hub/README.md index ace6a547c0..ba63c3282d 100644 --- a/apps/developer-hub/README.md +++ b/apps/developer-hub/README.md @@ -120,10 +120,96 @@ Update the `meta.json` file in each section to control navigation. Example: - Use proper heading hierarchy (h2, h3, etc.) - Link to related documentation when relevant +## API Reference Generation + +The API reference documentation is automatically generated from OpenAPI specifications using the `scripts/generate-docs.ts` script. This script converts OpenAPI specs (from services like Hermes and Fortuna) into MDX documentation files. + +### What It Does + +The script performs the following steps: + +1. **File Generation**: Uses `fumadocs-openapi` to convert OpenAPI specs into MDX files + + - Each API endpoint becomes a separate MDX file + - Files are organized by product (e.g., `pyth-core`, `entropy`) and service (e.g., `hermes`, `fortuna`) + +2. **Meta File Generation**: Creates `meta.json` files for navigation + + - Root `meta.json` for the API reference section + - Product-level `meta.json` files (e.g., `pyth-core/meta.json`) + - Service-level `meta.json` files (e.g., `pyth-core/hermes/meta.json`) + +3. **Post-Processing**: Customizes generated files to match our documentation structure + - Updates MDX frontmatter titles to use endpoint paths instead of operation IDs + - Rewrites `index.mdx` files to use `APICard` components with proper formatting + +### When to Run + +The script runs automatically during the build process (`pnpm turbo run build`). You typically don't need to run it manually unless: + +- You've updated an OpenAPI specification URL +- You've added a new service to the configuration +- You want to regenerate docs without doing a full build + +### Manual Execution + +To run the script manually: + +```bash +pnpm generate:docs +``` + +This will: + +- Fetch OpenAPI specs from the configured URLs +- Generate MDX files in `content/docs/api-reference/` +- Create/update all `meta.json` navigation files +- Post-process files to customize titles and index pages + +### Configuration + +To add a new API service, edit `src/lib/openapi.ts` and add an entry to the `products` object: + +```typescript +export const products = { + // ... existing services ... + newService: { + name: "newService", + product: "product-category", // e.g., "pyth-core" or "entropy" + openApiUrl: "https://api.example.com/docs/openapi.json", + }, +}; +``` + +After adding a new service: + +1. Run `pnpm generate:docs` to generate the documentation +2. The new service will appear in the API reference navigation + +### Generated Files + +All generated files are written to `content/docs/api-reference/`: + +``` +content/docs/api-reference/ +├── meta.json # Root navigation +├── pyth-core/ +│ ├── meta.json # Product navigation +│ └── hermes/ +│ ├── meta.json # Service navigation +│ ├── index.mdx # Service overview page +│ └── *.mdx # Individual endpoint pages +└── entropy/ + └── ... +``` + +**Important**: Generated files should not be edited manually. Any changes will be overwritten the next time the script runs. If you need to customize the documentation, modify the OpenAPI specification or the generation script itself. + ## Available Commands - `pnpm turbo run start:dev` - Start development server -- `pnpm turbo run build` - Build the project +- `pnpm turbo run build` - Build the project (includes API reference generation) +- `pnpm generate:docs` - Generate API reference documentation manually - `pnpm turbo run fix:format` - Format code with Prettier - `pnpm turbo run fix:lint:eslint` - Fix ESLint issues - `pnpm turbo run test:format` - Check formatting diff --git a/apps/developer-hub/content/docs/openapi/fortuna/chain_ids.mdx b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/chain_ids.mdx similarity index 63% rename from apps/developer-hub/content/docs/openapi/fortuna/chain_ids.mdx rename to apps/developer-hub/content/docs/api-reference/entropy/fortuna/chain_ids.mdx index 48444d5524..ff9dae3b8c 100644 --- a/apps/developer-hub/content/docs/openapi/fortuna/chain_ids.mdx +++ b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/chain_ids.mdx @@ -1,5 +1,5 @@ --- -title: Get the list of supported chain ids +title: "/v1/chains" description: Get the list of supported chain ids full: true _openapi: @@ -14,9 +14,4 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/openapi/fortuna/explorer.mdx b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/explorer.mdx similarity index 81% rename from apps/developer-hub/content/docs/openapi/fortuna/explorer.mdx rename to apps/developer-hub/content/docs/api-reference/entropy/fortuna/explorer.mdx index 8e8b4fd387..afee2b8223 100644 --- a/apps/developer-hub/content/docs/openapi/fortuna/explorer.mdx +++ b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/explorer.mdx @@ -1,5 +1,5 @@ --- -title: Returns the logs of all requests captured by the keeper. +title: "/v1/logs" description: >- Returns the logs of all requests captured by the keeper. @@ -32,9 +32,4 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/entropy/fortuna/index.mdx b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/index.mdx new file mode 100644 index 0000000000..6b6dbf7f31 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/index.mdx @@ -0,0 +1,9 @@ +--- +title: Overview +--- + + + + + + diff --git a/apps/developer-hub/content/docs/api-reference/entropy/fortuna/meta.json b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/meta.json new file mode 100644 index 0000000000..13ec2badc5 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/meta.json @@ -0,0 +1,9 @@ +{ + "title": "Fortuna", + "pages": [ + "index", + "chain_ids", + "revelation", + "explorer" + ] +} diff --git a/apps/developer-hub/content/docs/openapi/fortuna/revelation.mdx b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/revelation.mdx similarity index 82% rename from apps/developer-hub/content/docs/openapi/fortuna/revelation.mdx rename to apps/developer-hub/content/docs/api-reference/entropy/fortuna/revelation.mdx index a3e8711342..4d14f92027 100644 --- a/apps/developer-hub/content/docs/openapi/fortuna/revelation.mdx +++ b/apps/developer-hub/content/docs/api-reference/entropy/fortuna/revelation.mdx @@ -1,5 +1,5 @@ --- -title: Reveal the random value for a given sequence number and blockchain. +title: "/v1/chains/{chain_id}/revelations/{sequence}" description: >- Reveal the random value for a given sequence number and blockchain. @@ -44,11 +44,4 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/entropy/meta.json b/apps/developer-hub/content/docs/api-reference/entropy/meta.json new file mode 100644 index 0000000000..2c9bc438b8 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/entropy/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Entropy", + "pages": [ + "fortuna" + ] +} diff --git a/apps/developer-hub/content/docs/api-reference/index.mdx b/apps/developer-hub/content/docs/api-reference/index.mdx new file mode 100644 index 0000000000..a30a3d26c4 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/index.mdx @@ -0,0 +1,34 @@ +--- +title: API Reference +description: Complete API reference for Pyth Network services +--- + +import { IntegrationCard } from "../../../src/components/IntegrationCard"; +import { DiceSix, Database } from "@phosphor-icons/react/dist/ssr"; + +Welcome to the Pyth Network API Reference. Explore REST APIs for our core services. + +## Entropy + +
+ } + colorScheme="green" + /> +
+ +## Pyth Core + +
+ } + colorScheme="blue" + /> +
+ diff --git a/apps/developer-hub/content/docs/api-reference/meta.json b/apps/developer-hub/content/docs/api-reference/meta.json new file mode 100644 index 0000000000..cbf2de12c2 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/meta.json @@ -0,0 +1,9 @@ +{ + "root": true, + "title": "API Reference", + "icon": "Code", + "pages": [ + "entropy", + "pyth-core" + ] +} diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_price_feed.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_price_feed.mdx new file mode 100644 index 0000000000..9f7af043ad --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_price_feed.mdx @@ -0,0 +1,33 @@ +--- +title: "/api/get_price_feed" +description: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a price update for a price feed with a specific timestamp + + + Given a price feed id and timestamp, retrieve the Pyth price update closest to + that timestamp. +full: true +_openapi: + method: GET + route: /api/get_price_feed + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a price update for a price feed with a specific timestamp + + + Given a price feed id and timestamp, retrieve the Pyth price update + closest to that timestamp. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa.mdx new file mode 100644 index 0000000000..63c0d18297 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa.mdx @@ -0,0 +1,33 @@ +--- +title: "/api/get_vaa" +description: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a VAA for a price feed with a specific timestamp + + + Given a price feed id and timestamp, retrieve the Pyth price update closest to + that timestamp. +full: true +_openapi: + method: GET + route: /api/get_vaa + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a VAA for a price feed with a specific timestamp + + + Given a price feed id and timestamp, retrieve the Pyth price update + closest to that timestamp. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa_ccip.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa_ccip.mdx new file mode 100644 index 0000000000..a5945007bf --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/get_vaa_ccip.mdx @@ -0,0 +1,39 @@ +--- +title: "/api/get_vaa_ccip" +description: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a VAA for a price feed using CCIP + + + This endpoint accepts a single argument which is a hex-encoded byte string of + the following form: + + ` ` +full: true +_openapi: + method: GET + route: /api/get_vaa_ccip + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/updates/price/{publish_time} instead** + + + Get a VAA for a price feed using CCIP + + + This endpoint accepts a single argument which is a hex-encoded byte + string of the following form: + + ` ` +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/index.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/index.mdx new file mode 100644 index 0000000000..61556e66ed --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/index.mdx @@ -0,0 +1,18 @@ +--- +title: Overview +--- + + + + + + + + + + + + + + + diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_feeds.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_feeds.mdx new file mode 100644 index 0000000000..3d4b3fa5af --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_feeds.mdx @@ -0,0 +1,33 @@ +--- +title: "/api/latest_price_feeds" +description: >- + **Deprecated: use /v2/updates/price/latest instead** + + + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price for each + price feed. +full: true +_openapi: + method: GET + route: /api/latest_price_feeds + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/updates/price/latest instead** + + + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price + for each price feed. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_updates.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_updates.mdx new file mode 100644 index 0000000000..7efae3ad15 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_price_updates.mdx @@ -0,0 +1,27 @@ +--- +title: "/v2/updates/price/latest" +description: >- + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price for each + price feed. +full: true +_openapi: + method: GET + route: /v2/updates/price/latest + toc: [] + structuredData: + headings: [] + contents: + - content: >- + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price + for each price feed. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_publisher_stake_caps.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_publisher_stake_caps.mdx new file mode 100644 index 0000000000..0a80abd6aa --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_publisher_stake_caps.mdx @@ -0,0 +1,17 @@ +--- +title: "/v2/updates/publisher_stake_caps/latest" +description: Get the most recent publisher stake caps update data. +full: true +_openapi: + method: GET + route: /v2/updates/publisher_stake_caps/latest + toc: [] + structuredData: + headings: [] + contents: + - content: Get the most recent publisher stake caps update data. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_twaps.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_twaps.mdx new file mode 100644 index 0000000000..b2f61c001a --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_twaps.mdx @@ -0,0 +1,27 @@ +--- +title: "/v2/updates/twap/{window_seconds}/latest" +description: >- + Get the latest TWAP by price feed id with a custom time window. + + + Given a collection of price feed ids, retrieve the latest Pyth TWAP price for + each price feed. +full: true +_openapi: + method: GET + route: /v2/updates/twap/{window_seconds}/latest + toc: [] + structuredData: + headings: [] + contents: + - content: >- + Get the latest TWAP by price feed id with a custom time window. + + + Given a collection of price feed ids, retrieve the latest Pyth TWAP + price for each price feed. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_vaas.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_vaas.mdx new file mode 100644 index 0000000000..dc35023480 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/latest_vaas.mdx @@ -0,0 +1,43 @@ +--- +title: "/api/latest_vaas" +description: >- + **Deprecated: use /v2/updates/price/latest instead** + + + Get VAAs for a set of price feed ids. + + + Given a collection of price feed ids, retrieve the latest VAA for each. The + returned VAA(s) can + + be submitted to the Pyth contract to update the on-chain price. If VAAs are + not found for every + + provided price ID the call will fail. +full: true +_openapi: + method: GET + route: /api/latest_vaas + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/updates/price/latest instead** + + + Get VAAs for a set of price feed ids. + + + Given a collection of price feed ids, retrieve the latest VAA for + each. The returned VAA(s) can + + be submitted to the Pyth contract to update the on-chain price. If + VAAs are not found for every + + provided price ID the call will fail. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/meta.json b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/meta.json new file mode 100644 index 0000000000..8787df25f3 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/meta.json @@ -0,0 +1,18 @@ +{ + "title": "Hermes", + "pages": [ + "index", + "get_price_feed", + "get_vaa", + "get_vaa_ccip", + "latest_price_feeds", + "latest_vaas", + "price_feed_ids", + "price_feeds_metadata", + "latest_price_updates", + "price_stream_sse_handler", + "timestamp_price_updates", + "latest_publisher_stake_caps", + "latest_twaps" + ] +} diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feed_ids.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feed_ids.mdx new file mode 100644 index 0000000000..9a07572bbc --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feed_ids.mdx @@ -0,0 +1,33 @@ +--- +title: "/api/price_feed_ids" +description: >- + **Deprecated: use /v2/price_feeds instead** + + + Get the set of price feed IDs. + + + This endpoint fetches all of the price feed IDs for which price updates can be + retrieved. +full: true +_openapi: + method: GET + route: /api/price_feed_ids + toc: [] + structuredData: + headings: [] + contents: + - content: >- + **Deprecated: use /v2/price_feeds instead** + + + Get the set of price feed IDs. + + + This endpoint fetches all of the price feed IDs for which price + updates can be retrieved. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feeds_metadata.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feeds_metadata.mdx new file mode 100644 index 0000000000..d12730307e --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_feeds_metadata.mdx @@ -0,0 +1,31 @@ +--- +title: "/v2/price_feeds" +description: >- + Get the set of price feeds. + + + This endpoint fetches all price feeds from the Pyth network. It can be + filtered by asset type + + and query string. +full: true +_openapi: + method: GET + route: /v2/price_feeds + toc: [] + structuredData: + headings: [] + contents: + - content: >- + Get the set of price feeds. + + + This endpoint fetches all price feeds from the Pyth network. It can be + filtered by asset type + + and query string. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_stream_sse_handler.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_stream_sse_handler.mdx new file mode 100644 index 0000000000..21dceff295 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/price_stream_sse_handler.mdx @@ -0,0 +1,33 @@ +--- +title: "/v2/updates/price/stream" +description: >- + SSE route handler for streaming price updates. + + + The connection will automatically close after 24 hours to prevent resource + leaks. + + Clients should implement reconnection logic to maintain continuous price + updates. +full: true +_openapi: + method: GET + route: /v2/updates/price/stream + toc: [] + structuredData: + headings: [] + contents: + - content: >- + SSE route handler for streaming price updates. + + + The connection will automatically close after 24 hours to prevent + resource leaks. + + Clients should implement reconnection logic to maintain continuous + price updates. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/timestamp_price_updates.mdx b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/timestamp_price_updates.mdx new file mode 100644 index 0000000000..297a41392a --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/hermes/timestamp_price_updates.mdx @@ -0,0 +1,27 @@ +--- +title: "/v2/updates/price/{publish_time}" +description: >- + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price for each + price feed. +full: true +_openapi: + method: GET + route: /v2/updates/price/{publish_time} + toc: [] + structuredData: + headings: [] + contents: + - content: >- + Get the latest price updates by price feed id. + + + Given a collection of price feed ids, retrieve the latest Pyth price + for each price feed. +--- + +{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} + + \ No newline at end of file diff --git a/apps/developer-hub/content/docs/api-reference/pyth-core/meta.json b/apps/developer-hub/content/docs/api-reference/pyth-core/meta.json new file mode 100644 index 0000000000..34888f85e1 --- /dev/null +++ b/apps/developer-hub/content/docs/api-reference/pyth-core/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Pyth Core", + "pages": [ + "hermes" + ] +} diff --git a/apps/developer-hub/content/docs/meta.json b/apps/developer-hub/content/docs/meta.json index 0d89d9cdc8..6af1ac7229 100644 --- a/apps/developer-hub/content/docs/meta.json +++ b/apps/developer-hub/content/docs/meta.json @@ -1,3 +1,3 @@ { - "pages": ["price-feeds", "express-relay", "entropy"] + "pages": ["price-feeds", "express-relay", "entropy", "api-reference"] } diff --git a/apps/developer-hub/content/docs/openapi/fortuna/index.mdx b/apps/developer-hub/content/docs/openapi/fortuna/index.mdx deleted file mode 100644 index 483df2b0b0..0000000000 --- a/apps/developer-hub/content/docs/openapi/fortuna/index.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Overview ---- - -{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - - - - - - diff --git a/apps/developer-hub/package.json b/apps/developer-hub/package.json index cf6cea95e8..9317e02ecb 100644 --- a/apps/developer-hub/package.json +++ b/apps/developer-hub/package.json @@ -18,7 +18,7 @@ "test:lint:eslint": "eslint . --max-warnings 0", "test:lint:stylelint": "stylelint 'src/**/*.scss' --max-warnings 0", "test:types": "tsc", - "generate:docs": "bun ./scripts/generate-docs.ts" + "generate:docs": "tsx ./scripts/generate-docs.ts" }, "dependencies": { "@phosphor-icons/react": "catalog:", @@ -48,6 +48,7 @@ "remark-math": "^6.0.0", "remark-mdx": "^3.1.1", "shiki": "catalog:", + "tsx": "catalog:", "viem": "catalog:", "zod": "catalog:", "zod-validation-error": "catalog:" diff --git a/apps/developer-hub/scripts/generate-docs.ts b/apps/developer-hub/scripts/generate-docs.ts index 03e0a9ac5f..d1d624d894 100644 --- a/apps/developer-hub/scripts/generate-docs.ts +++ b/apps/developer-hub/scripts/generate-docs.ts @@ -1,50 +1,580 @@ +/** + * API Reference Documentation Generator + * + * This script automatically generates API reference documentation from OpenAPI specifications. + * It converts OpenAPI specs (from services like Hermes and Fortuna) into MDX documentation + * files that are used by the Fumadocs documentation system. + * + * + * ## Usage + * + * This script runs automatically during the build process. To run it manually: + * + * ```bash + * pnpm generate:docs + * ``` + * + * ## Configuration + * + * To add a new API service, add it to `src/lib/openapi.ts` in the `products` object. + * Each service needs: + * - `name`: Service identifier (e.g., "hermes") + * - `product`: Product category (e.g., "pyth-core") + * - `openApiUrl`: URL to the OpenAPI specification JSON + * + */ + +import * as fs from "node:fs/promises"; +import path from "node:path"; + import { generateFiles } from "fumadocs-openapi"; +import { createOpenAPI } from "fumadocs-openapi/server"; -import { openapi, products } from "../src/lib/openapi"; - -const outDir = "./content/docs/openapi/"; - -export async function generateDocs() { - await generateFiles({ - input: openapi, - output: outDir, - per: "operation", - name: (output, document) => { - // Extract product name from the OpenAPI document info - const productName = getProductName(document.info.title); - - if (output.type === "operation") { - const operation = - document.paths?.[output.item.path]?.[output.item.method]; - const operationId = - operation?.operationId ?? - output.item.path.replaceAll(/[^a-zA-Z0-9]/g, "_"); - return `${productName}/${operationId}`; - } +import { products } from "../src/lib/openapi"; - return `${productName}/webhooks/${output.item.name}`; - }, - index: { - url: { - baseUrl: "/openapi/", - contentDir: "./content/docs/openapi", +const OUTPUT_DIR = "./content/docs/api-reference/"; + +const generatedEndpoints: Record = {}; + +type ApiCardData = { + href: string; + route: string; + method: string; + description: string; +}; + +type MetaFile = { + root?: boolean; + title: string; + icon?: string; + pages: string[]; +}; + +export async function generateDocs(): Promise { + // eslint-disable-next-line no-console + console.log("Starting API reference documentation generation...\n"); + + await generateMdxFilesFromOpenApi(); + + await generateMetaFiles(); + + await generateApiReferenceIndex(); + + await updateMdxTitles(); + + await updateIndexCards(); + + // eslint-disable-next-line no-console + console.log("\nDocumentation generation complete!"); +} + +// ============================================================================ +// File Generation +// ============================================================================ + +/** + * Generates MDX documentation files from OpenAPI specifications. + * + * Processes each service separately to ensure: + * - Index files only contain endpoints from that specific service + * - Each service can have its own OpenAPI spec URL + * - Generated files are organized by product/service hierarchy + * + * For each service: + * - Creates an OpenAPI instance from the service's spec URL + * - Generates one MDX file per API operation + * - Tracks generated operation IDs for later use in meta files + * - Creates an index.mdx file listing all endpoints + */ +async function generateMdxFilesFromOpenApi(): Promise { + // eslint-disable-next-line no-console + console.log("Generating MDX files from OpenAPI specifications..."); + + for (const [serviceName, config] of Object.entries(products)) { + // eslint-disable-next-line no-console + console.log(`\n Processing service: ${serviceName}`); + + generatedEndpoints[serviceName] = []; + + const serviceOpenapi = createOpenAPI({ + input: [config.openApiUrl], + }); + + // Generate MDX files using fumadocs-openapi + await generateFiles({ + input: serviceOpenapi, + output: OUTPUT_DIR, + per: "operation", // One file per API operation + name: (output, document) => { + // Generate file name based on operation type + if (output.type === "operation") { + return generateOperationFileName( + output, + document, + serviceName, + config.product, + ); + } + + return `${config.product}/${serviceName}/webhooks/${output.item.name}`; }, - items: Object.keys(products).map((productName) => ({ - path: `${productName}/index.mdx`, - })), + frontmatter: (context) => { + const ctx = context as { type?: string; path?: string }; + if (ctx.type === "operation" && ctx.path) { + return { + title: ctx.path, + }; + } + return {}; + }, + index: { + url: { + baseUrl: "/api-reference/", + contentDir: "./content/docs/api-reference", + }, + items: [ + { + path: `${config.product}/${serviceName}/index.mdx`, + }, + ], + }, + }); + } +} + +function generateOperationFileName( + output: { item: { path: string; method: string } }, + document: unknown, + serviceName: string, + productName: string, +): string { + const doc = document as { + paths?: Record< + string, + Record | undefined + >; + }; + const operation = doc.paths?.[output.item.path]?.[output.item.method]; + + const operationId = + operation?.operationId ?? output.item.path.replaceAll(/[^a-zA-Z0-9]/g, "_"); + + // Track this endpoint for meta file generation + generatedEndpoints[serviceName]?.push(operationId); + + // eslint-disable-next-line no-console + console.log(` ✓ ${operationId}`); + + // Return file path: product/service/operationId + return `${productName}/${serviceName}/${operationId}`; +} + +/** + * Generates all meta.json files for navigation structure. + * + * Creates a three-level hierarchy: + * 1. Root meta.json: Lists all product categories (e.g., pyth-core, entropy) + * 2. Product meta.json: Lists all services in that product (e.g., hermes in pyth-core) + * 3. Service meta.json: Lists all endpoints in that service (e.g., get_price_feed in hermes) + * + * These meta.json files are used by Fumadocs to build the navigation sidebar. + */ +async function generateMetaFiles(): Promise { + // eslint-disable-next-line no-console + console.log("\nGenerating meta.json navigation files..."); + + const productGroups = groupServicesByProduct(); + + await generateRootMetaFile(productGroups); + + await generateProductAndServiceMetaFiles(productGroups); +} + +function groupServicesByProduct(): Record { + const productGroups: Record = {}; + + for (const [serviceName, config] of Object.entries(products)) { + productGroups[config.product] ??= []; + productGroups[config.product]?.push(serviceName); + } + + return productGroups; +} + +async function generateRootMetaFile( + productGroups: Record, +): Promise { + const rootMeta: MetaFile = { + root: true, + title: "API Reference", + icon: "Code", + pages: Object.keys(productGroups), + }; + + await writeJson(path.join(OUTPUT_DIR, "meta.json"), rootMeta); + // eslint-disable-next-line no-console + console.log(" ✓ api-reference/meta.json"); +} + +async function generateProductAndServiceMetaFiles( + productGroups: Record, +): Promise { + for (const [productName, services] of Object.entries(productGroups)) { + const productMeta: MetaFile = { + title: formatProductTitle(productName), + pages: services, + }; + + const productDir = path.join(OUTPUT_DIR, productName); + await fs.mkdir(productDir, { recursive: true }); + await writeJson(path.join(productDir, "meta.json"), productMeta); + // eslint-disable-next-line no-console + console.log(` ✓ ${productName}/meta.json`); + + // Generate service-level meta.json files + for (const serviceName of services) { + const endpoints = generatedEndpoints[serviceName] ?? []; + const serviceMeta: MetaFile = { + title: formatServiceTitle(serviceName), + pages: ["index", ...endpoints], + }; + + const serviceDir = path.join(productDir, serviceName); + await writeJson(path.join(serviceDir, "meta.json"), serviceMeta); + // eslint-disable-next-line no-console + console.log(` ✓ ${productName}/${serviceName}/meta.json`); + } + } +} + +function formatProductTitle(productName: string): string { + return productName + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +function formatServiceTitle(serviceName: string): string { + return serviceName.charAt(0).toUpperCase() + serviceName.slice(1); +} + +/** + * Generates the API Reference index page (content/docs/api-reference/index.mdx). + * + * Creates a page that lists all products and their associated services using + * IntegrationCard components in a simple grid layout. + */ +async function generateApiReferenceIndex(): Promise { + // eslint-disable-next-line no-console + console.log("\nGenerating API Reference index page..."); + + const productGroups = groupServicesByProduct(); + + // Map service names to their configurations and metadata + const serviceMetadata: Record< + string, + { + name: string; + product: string; + icon: string; + colorScheme: "green" | "blue" | "purple" | "yellow"; + description: string; + } + > = { + fortuna: { + name: "fortuna", + product: "entropy", + icon: "DiceSix", + colorScheme: "green", + description: "Random number generation API with callback support", }, - }); + hermes: { + name: "hermes", + product: "pyth-core", + icon: "Database", + colorScheme: "blue", + description: "REST API for accessing price feeds and updates", + }, + }; + + // Generate product sections + const productSections: string[] = []; + + for (const [productName, services] of Object.entries(productGroups)) { + const productTitle = formatProductTitle(productName); + const serviceCards: string[] = []; + + for (const serviceName of services) { + const metadata = serviceMetadata[serviceName]; + if (!metadata) continue; + + const serviceTitle = formatServiceTitle(serviceName); + const serviceHref = `/api-reference/${productName}/${serviceName}`; + + serviceCards.push( + ` } + colorScheme="${metadata.colorScheme}" + />`, + ); + } + + if (serviceCards.length > 0) { + productSections.push(`## ${productTitle} + +
+${serviceCards.join("\n")} +
+`); + } + } + + // Generate the complete MDX content + const indexContent = `--- +title: API Reference +description: Complete API reference for Pyth Network services +--- + +import { IntegrationCard } from "../../../src/components/IntegrationCard"; +import { DiceSix, Database } from "@phosphor-icons/react/dist/ssr"; + +Welcome to the Pyth Network API Reference. Explore REST APIs for our core services. + +${productSections.join("\n")} +`; + + const indexPath = path.join(OUTPUT_DIR, "index.mdx"); + await fs.writeFile(indexPath, indexContent); + // eslint-disable-next-line no-console + console.log(" ✓ api-reference/index.mdx"); +} + +async function updateMdxTitles(): Promise { + // eslint-disable-next-line no-console + console.log("\nUpdating MDX file titles to use endpoint paths..."); + + for (const [serviceName, config] of Object.entries(products)) { + const serviceDir = path.join(OUTPUT_DIR, config.product, serviceName); + + try { + const files = await fs.readdir(serviceDir); + + for (const file of files) { + // Skip non-MDX files and index files + if (!file.endsWith(".mdx") || file === "index.mdx") continue; + + await updateSingleMdxTitle( + serviceDir, + file, + config.product, + serviceName, + ); + } + } catch { + // Directory might not exist if no endpoints were generated + // This is fine, just skip this service + } + } +} + +/** + * Updates the title in a single MDX file to use the endpoint route. + * + * @param serviceDir - Directory containing the MDX file + * @param fileName - Name of the MDX file to update + * @param productName - Product name for logging + * @param serviceName - Service name for logging + */ +async function updateSingleMdxTitle( + serviceDir: string, + fileName: string, + productName: string, + serviceName: string, +): Promise { + const filePath = path.join(serviceDir, fileName); + const content = await fs.readFile(filePath, "utf8"); + + const routeRegex = /route:\s*([^\n]+)/; + const routeMatch = routeRegex.exec(content); + + if (!routeMatch?.[1]) { + // No route found, skip this file + return; + } + + const route = routeMatch[1].trim(); + + const updatedContent = content.replace( + /^---\ntitle:\s*[^\n]+/, + `---\ntitle: "${route}"`, + ); + + // Only write if content actually changed + if (updatedContent !== content) { + await fs.writeFile(filePath, updatedContent); + // eslint-disable-next-line no-console + console.log(` ✓ ${productName}/${serviceName}/${fileName}`); + } +} + +/** + * Updates index.mdx files to use APICard components. + **/ +async function updateIndexCards(): Promise { + // eslint-disable-next-line no-console + console.log("\nUpdating index pages with APICard components..."); + + for (const [serviceName, config] of Object.entries(products)) { + const serviceDir = path.join(OUTPUT_DIR, config.product, serviceName); + const indexPath = path.join(serviceDir, "index.mdx"); + + // Skip if index file doesn't exist + try { + await fs.access(indexPath); + } catch { + continue; + } + + // Extract card data from all endpoint files + const cardData = await extractApiCardData( + serviceDir, + serviceName, + config.product, + ); + + // Generate new index content with APICard components + const newIndexContent = generateIndexContent(cardData); + + await fs.writeFile(indexPath, newIndexContent); + // eslint-disable-next-line no-console + console.log(` ✓ ${config.product}/${serviceName}/index.mdx`); + } +} + +async function extractApiCardData( + serviceDir: string, + serviceName: string, + productName: string, +): Promise { + const endpoints = generatedEndpoints[serviceName] ?? []; + const cardData: ApiCardData[] = []; + + for (const operationId of endpoints) { + const mdxPath = path.join(serviceDir, `${operationId}.mdx`); + + try { + const content = await fs.readFile(mdxPath, "utf8"); + const card = extractCardDataFromMdx( + content, + operationId, + productName, + serviceName, + ); + + if (card) { + cardData.push(card); + } + } catch { + // File doesn't exist, skip it + // This can happen if generation failed for this endpoint + } + } + + return cardData; +} + +function extractCardDataFromMdx( + content: string, + operationId: string, + productName: string, + serviceName: string, +): ApiCardData | null { + const routeMatch = /route:\s*([^\n]+)/.exec(content); + const route = routeMatch?.[1]?.trim() ?? operationId; + + const methodMatch = /method:\s*([^\n]+)/.exec(content); + const method = methodMatch?.[1]?.trim().toUpperCase() ?? "GET"; + + const description = extractDescriptionFromFrontmatter(content); + + return { + href: `/api-reference/${productName}/${serviceName}/${operationId}`, + route, + method, + description, + }; +} + +function extractDescriptionFromFrontmatter(content: string): string { + let descText = ""; + + const multilineMatch = /description:\s*>-\s*\n((?:\s{2}.*\n)+)/.exec(content); + if (multilineMatch?.[1]) { + descText = multilineMatch[1] + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .join(" "); + } else { + // Try single-line format: description: text + const singleMatch = /description:\s*([^\n]+)/.exec(content); + if (singleMatch?.[1]) { + descText = singleMatch[1].trim(); + } + } + + // Extract first sentence and clean formatting + return getFirstSentence(descText); } -function getProductName(title: string) { - // Match the title to a product name - const titleLower = title.toLowerCase(); - for (const [name] of Object.entries(products)) { - if (titleLower.includes(name)) { - return name; +function generateIndexContent(cardData: ApiCardData[]): string { + const cards = cardData + .map( + (card) => + ` `, + ) + .join("\n"); + + return `--- +title: Overview +--- + + +${cards} + +`; +} + +function getFirstSentence(text: string): string { + if (!text) return ""; + + let cleaned = text.replaceAll(/\*\*[^*]+\*\*/g, "").trim(); + + if (cleaned.toLowerCase().startsWith("deprecated")) { + const afterDeprecated = cleaned.indexOf(")"); + if (afterDeprecated > 0) { + cleaned = cleaned.slice(afterDeprecated + 1).trim(); } } - return "unknown"; + + const sentenceEnd = cleaned.search(/[.!?](\s|$)/); + if (sentenceEnd === -1) { + return cleaned.trim(); + } + + return cleaned.slice(0, sentenceEnd + 1).trim(); +} + +function escapeQuotes(text: string): string { + return text.replaceAll('"', String.raw`\"`); +} + +async function writeJson(filePath: string, data: object): Promise { + const jsonContent = JSON.stringify(data, undefined, 2) + "\n"; + await fs.writeFile(filePath, jsonContent); } await generateDocs(); diff --git a/apps/developer-hub/src/components/APICard/index.module.scss b/apps/developer-hub/src/components/APICard/index.module.scss new file mode 100644 index 0000000000..8b12c895a0 --- /dev/null +++ b/apps/developer-hub/src/components/APICard/index.module.scss @@ -0,0 +1,77 @@ +@use "@pythnetwork/component-library/theme"; + +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: theme.spacing(4); + + @include theme.breakpoint("md") { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 768px) { + grid-template-columns: 1fr; + } +} + +.card { + display: flex; + flex-direction: column; + gap: theme.spacing(2); + padding: theme.spacing(5); + border-radius: theme.border-radius("lg"); + background-color: theme.color("background", "secondary"); + text-decoration: none; + transition: background-color 200ms ease-out; + + &:hover { + background-color: theme.color("background", "card-highlight"); + } +} + +.title { + display: flex; + align-items: center; + gap: theme.spacing(2); + flex-wrap: wrap; +} + +.name { + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("semibold"); + color: theme.color("foreground"); +} + +.badge { + font-size: theme.font-size("xxs"); + font-weight: theme.font-weight("semibold"); + text-transform: uppercase; + letter-spacing: theme.letter-spacing("wide"); +} + +.get { + color: theme.pallette-color("green", 400); +} + +.post { + color: theme.pallette-color("amber", 400); +} + +.put { + color: theme.pallette-color("blue", 400); +} + +.patch { + color: theme.pallette-color("purple", 400); +} + +.delete { + color: theme.pallette-color("red", 400); +} + +.description { + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("normal"); + line-height: 1.6; + color: theme.color("muted"); +} diff --git a/apps/developer-hub/src/components/APICard/index.tsx b/apps/developer-hub/src/components/APICard/index.tsx new file mode 100644 index 0000000000..3cbefe0294 --- /dev/null +++ b/apps/developer-hub/src/components/APICard/index.tsx @@ -0,0 +1,45 @@ +import clsx from "clsx"; +import Link from "next/link"; +import type { ReactNode } from "react"; + +import styles from "./index.module.scss"; + +type APICardProps = { + href: string; + title: string; + method: string; + description?: string; +}; + +type APICardsProps = { + children: ReactNode; +}; + +export function APICards({ children }: APICardsProps) { + return
{children}
; +} + +export function APICard({ href, title, method, description }: APICardProps) { + const methodLower = method.toLowerCase(); + + return ( + +
+ {title} + + {method} + +
+ {description &&

{description}

} + + ); +} diff --git a/apps/developer-hub/src/components/Pages/BasePage/index.tsx b/apps/developer-hub/src/components/Pages/BasePage/index.tsx index e9e517356d..e8e9dc5fa5 100644 --- a/apps/developer-hub/src/components/Pages/BasePage/index.tsx +++ b/apps/developer-hub/src/components/Pages/BasePage/index.tsx @@ -20,6 +20,9 @@ export async function BasePage(props: { params: { slug: string[] } }) { const title = page.data.title; const url = page.url; + // Hide PageActions for api-reference pages + const isApiReference = url.startsWith("/api-reference"); + return ( {page.data.title} {page.data.description} - + {!isApiReference && ( + + )} diff --git a/apps/developer-hub/src/components/Root/global.css b/apps/developer-hub/src/components/Root/global.css index 7af62f670a..8dabe45a95 100644 --- a/apps/developer-hub/src/components/Root/global.css +++ b/apps/developer-hub/src/components/Root/global.css @@ -22,6 +22,7 @@ @import "tailwindcss"; @import "fumadocs-ui/css/neutral.css"; @import "fumadocs-ui/css/preset.css"; +@import "fumadocs-openapi/css/preset.css"; /* @import "./theme.css"; this overrides the default colors to match the pyth branding */ /* @import "./fumadocs-global-style-overrides.css"; */ diff --git a/apps/developer-hub/src/lib/openapi.ts b/apps/developer-hub/src/lib/openapi.ts index 5ffdb8629d..e2a7dd8ab5 100644 --- a/apps/developer-hub/src/lib/openapi.ts +++ b/apps/developer-hub/src/lib/openapi.ts @@ -3,8 +3,14 @@ import { createOpenAPI } from "fumadocs-openapi/server"; export const products = { fortuna: { name: "fortuna", + product: "entropy", openApiUrl: "https://fortuna-staging.dourolabs.app/docs/openapi.json", }, + hermes: { + name: "hermes", + product: "pyth-core", + openApiUrl: "https://hermes.pyth.network/docs/openapi.json", + }, }; export const openapi = createOpenAPI({ diff --git a/apps/developer-hub/src/mdx-components.tsx b/apps/developer-hub/src/mdx-components.tsx index 20240cb85e..886d04ac6b 100644 --- a/apps/developer-hub/src/mdx-components.tsx +++ b/apps/developer-hub/src/mdx-components.tsx @@ -5,6 +5,7 @@ import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import defaultMdxComponents from "fumadocs-ui/mdx"; import type { MDXComponents } from "mdx/types"; +import { APICard, APICards } from "./components/APICard"; import { openapi } from "./lib/openapi"; export function getMDXComponents(components?: MDXComponents): MDXComponents { @@ -13,6 +14,8 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { APIPage: (props: ApiPageProps) => ( ), + APICard, + APICards, Tabs, Tab, ...components, diff --git a/apps/developer-hub/turbo.json b/apps/developer-hub/turbo.json index a95e4e0859..fe7e6b2c75 100644 --- a/apps/developer-hub/turbo.json +++ b/apps/developer-hub/turbo.json @@ -3,12 +3,17 @@ "extends": ["//"], "tasks": { "build": { + "dependsOn": ["generate:docs"], "env": [ "VERCEL_ENV", "GOOGLE_ANALYTICS_ID", "DISABLE_ACCESSIBILITY_REPORTING" ] }, + "generate:docs": { + "dependsOn": ["//#install:modules", "^build"], + "outputs": ["content/docs/api-reference/**"] + }, "fix:lint": { "dependsOn": [ "//#install:modules", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f3c1862ac..438bcd62f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -647,6 +647,9 @@ importers: shiki: specifier: 'catalog:' version: 3.12.2 + tsx: + specifier: 'catalog:' + version: 4.20.6 viem: specifier: 'catalog:' version: 2.38.2(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@6.0.3)(zod@3.24.4)