From d2539d06e38854546d1c5adbf2c92f000ce31541 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 16:34:09 -0700 Subject: [PATCH 01/13] add md route logic --- public/llms.txt | 327 +++++++++++++++++++++----------------- src/pages/[...path].md.ts | 172 ++++++++++++++++++++ 2 files changed, 349 insertions(+), 150 deletions(-) create mode 100644 src/pages/[...path].md.ts diff --git a/public/llms.txt b/public/llms.txt index 4998fcc16b5..6d442ebb1f9 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,173 +1,200 @@ # Chainlink Documentation -> Chainlink is the industry-standard oracle platform bringing the capital markets onchain and powering the majority of decentralized finance (DeFi). The Chainlink stack provides the essential data, interoperability, compliance, and privacy standards needed to power advanced blockchain use cases for institutional tokenized assets, lending, payments, stablecoins, and more. Since inventing decentralized oracle networks, Chainlink has enabled tens of trillions in transaction value and now secures the vast majority of DeFi. -Many of the world’s largest financial services institutions have also adopted Chainlink’s standards and infrastructure, including Swift, Euroclear, Mastercard, Fidelity International, UBS, S&P Dow Jones Indices, FTSE Russell, WisdomTree, ANZ, and top protocols such as Aave, Lido, GMX and many others. Chainlink leverages a novel fee model where offchain and onchain revenue from enterprise adoption is converted to LINK tokens and stored in a strategic [Chainlink Reserve](https://blog.chain.link/chainlink-reserve-strategic-link-reserve/). Learn more at [chain.link](https://chain.link/). +Use this file as the root navigation index for Chainlink docs. + +For each task: + +1. Retrieve the smallest relevant `.md` page first. +2. Prefer reference, supported networks, directory, billing, service limits, and address pages when they provide complete content. Otherwise use the closest fully rendered documentation page. +3. Do not rely on memorized addresses, supported networks, router addresses, feed addresses, chain selectors, quotas, or billing details. +4. Use product-level `llms-full.txt` files only when broad product context is required. + +Current defaults: + +* Prefer CRE for new multi-step or offchain workflow use cases unless another product is explicitly required. +* Prefer VRF v2.5 for new randomness integrations. +* Treat Any API and `/chainlink-nodes/v1/` as legacy unless maintaining an existing integration. -This document offers simple, clean, and comprehensive resources for developers to learn, experiment, and build with Chainlink. ## Core Documentation -- [Chainlink Docs](https://docs.chain.link/): Main documentation hub for all Chainlink products and services. + +* https://docs.chain.link/getting-started/conceptual-overview.md +* https://docs.chain.link/builders-quick-links.md +* https://docs.chain.link/oracle-platform/overview.md +* https://docs.chain.link/oracle-platform/data-standard.md +* https://docs.chain.link/oracle-platform/interoperability-standard.md +* https://docs.chain.link/oracle-platform/compliance-standard.md +* https://docs.chain.link/oracle-platform/privacy-standard.md + ## Chainlink Runtime Environment (CRE) -- [About CRE](https://docs.chain.link/cre/about): Overview of the Chainlink Runtime Environment and its capabilities. -- [Key Terms and Concepts](https://docs.chain.link/cre/key-terms-and-concepts): Glossary of foundational terms used in CRE. -- [Service Quotas](https://docs.chain.link/cre/service-quotas): Information about usage limits and quotas. -- [Support & Feedback](https://docs.chain.link/cre/support-feedback): How to get help and provide feedback. -- [Release Notes](https://docs.chain.link/cre/release-notes): Version history and updates for CRE. -- [Overview](https://docs.chain.link/cre/getting-started/overview): Introduction to the CRE development workflow. -- [Triggers](https://docs.chain.link/cre/workflow-guides/triggers): Configuring and using triggers. -- [EVM Chain Interactions](https://docs.chain.link/cre/workflow-guides/evm-chain-interactions): Interacting with EVM-compatible chains. -- [API Interactions](https://docs.chain.link/cre/workflow-guides/api-interactions): Using APIs within CRE workflows. -- [Secrets](https://docs.chain.link/cre/workflow-guides/secrets): Managing secrets securely within CRE workflows. -- [Simulating Workflows](https://docs.chain.link/cre/workflow-operations/simulating-workflows): Running simulations of workflows. -- [Deploying Workflows](https://docs.chain.link/cre/workflow-operations/deploying-workflows): Deploying workflows to the CRE environment. -- [Activating & Pausing Workflows](https://docs.chain.link/cre/workflow-operations/activating-pausing-workflows): Managing workflow lifecycle states. -- [Updating Deployed Workflows](https://docs.chain.link/cre/workflow-operations/updating-deployed-workflows): Modifying live workflows. -- [Deleting Workflows](https://docs.chain.link/cre/workflow-operations/deleting-workflows): Removing workflows from CRE. -- [Using Multi-sig Wallets](https://docs.chain.link/cre/workflow-operations/using-multisig-wallets): Managing workflow ownership via multi-signature wallets. -- [Monitoring & Debugging Workflows](https://docs.chain.link/cre/workflow-operations/monitoring-debugging-workflows): Observing and troubleshooting workflows. -- [Account](https://docs.chain.link/cre/account-organization/account): Managing your CRE account. -- [Organization](https://docs.chain.link/cre/account-organization/organization): Managing organization-level settings. -- [Overview](https://docs.chain.link/cre/capabilities/overview): Overview of available capabilities. -- [Consensus Computing](https://docs.chain.link/cre/concepts/consensus-computing): How consensus computing operates in CRE. -- [Non-Determinism in Workflows](https://docs.chain.link/cre/concepts/non-determinism-in-workflows): Handling non-deterministic operations. -- [Time in CRE](https://docs.chain.link/cre/concepts/time): Managing time and scheduling. -- [Random in CRE](https://docs.chain.link/cre/concepts/random): Using randomness within workflows. -- [Running a Demo Workflow](https://docs.chain.link/cre/templates/running-a-demo-workflow): Example workflow template for testing and learning. -- [Project Configuration](https://docs.chain.link/cre/reference/project-configuration): Configuration reference for CRE projects. -- [CLI Reference](https://docs.chain.link/cre/reference/cli): Full CLI command reference. -- [SDK Reference](https://docs.chain.link/cre/reference/sdk): SDK reference documentation. +* https://docs.chain.link/cre.md +* https://docs.chain.link/cre/key-terms.md +* https://docs.chain.link/cre/service-quotas.md +* https://docs.chain.link/cre/supported-networks-go.md +* https://docs.chain.link/cre/supported-networks-ts.md +* https://docs.chain.link/cre/support-feedback.md +* https://docs.chain.link/cre/release-notes.md + +### Getting Started + +* https://docs.chain.link/cre/getting-started/overview.md +* https://docs.chain.link/cre/getting-started/cli-installation.md +* https://docs.chain.link/cre/getting-started/before-you-build-go.md +* https://docs.chain.link/cre/getting-started/before-you-build-ts.md +* https://docs.chain.link/cre/getting-started/build-with-ai-go.md +* https://docs.chain.link/cre/getting-started/build-with-ai-ts.md + +### Capabilities + +* https://docs.chain.link/cre/capabilities.md +* https://docs.chain.link/cre/capabilities/triggers.md +* https://docs.chain.link/cre/capabilities/http.md +* https://docs.chain.link/cre/capabilities/evm-read-write.md +* https://docs.chain.link/cre/capabilities/confidential-http-go.md +* https://docs.chain.link/cre/capabilities/confidential-http-ts.md + +### Workflow Guides + +* https://docs.chain.link/cre/guides/workflow/using-triggers/overview.md +* https://docs.chain.link/cre/guides/workflow/using-http-client.md +* https://docs.chain.link/cre/guides/workflow/using-confidential-http-client.md +* https://docs.chain.link/cre/guides/workflow/using-evm-client/overview-go.md +* https://docs.chain.link/cre/guides/workflow/using-evm-client/overview-ts.md +* https://docs.chain.link/cre/guides/workflow/secrets.md +* https://docs.chain.link/cre/guides/workflow/using-randomness.md + +### Operations + +* https://docs.chain.link/cre/guides/operations/simulating-workflows.md +* https://docs.chain.link/cre/guides/operations/deploying-workflows.md +* https://docs.chain.link/cre/guides/operations/activating-pausing-workflows.md +* https://docs.chain.link/cre/guides/operations/updating-deployed-workflows.md +* https://docs.chain.link/cre/guides/operations/deleting-workflows.md +* https://docs.chain.link/cre/guides/operations/using-multisig-wallets.md +* https://docs.chain.link/cre/guides/operations/monitoring-workflows.md + +### Account & Templates + +* https://docs.chain.link/cre/account.md +* https://docs.chain.link/cre/organization.md +* https://docs.chain.link/cre/templates.md +* https://docs.chain.link/cre/templates/running-demo-workflow-go.md +* https://docs.chain.link/cre/templates/running-demo-workflow-ts.md + +### Reference + +* https://docs.chain.link/cre/reference/cli.md +* https://docs.chain.link/cre/reference/project-configuration-go.md +* https://docs.chain.link/cre/reference/project-configuration-ts.md +* https://docs.chain.link/cre/reference/sdk/overview-go.md +* https://docs.chain.link/cre/reference/sdk/overview-ts.md ## Cross-Chain Interoperability Protocol (CCIP) -- [CCIP Overview](https://docs.chain.link/ccip): Introduction to CCIP and its functionalities. -- [Getting Started with CCIP](https://docs.chain.link/ccip/getting-started): Step-by-step guide to begin using CCIP. -- [CCIP API Reference](https://docs.chain.link/ccip/api-reference): Detailed API documentation for CCIP. -- [CCIP Architecture](https://docs.chain.link/ccip/architecture): Technical architecture and design of CCIP. -- [CCIP Best Practices](https://docs.chain.link/ccip/best-practices): Recommended practices for implementing CCIP. -- [CCIP Billing](https://docs.chain.link/ccip/billing): Information on billing and pricing for CCIP services. -- [CCIP Concepts](https://docs.chain.link/ccip/concepts): Fundamental concepts and components of CCIP. -- [Cross-Chain Tokens](https://docs.chain.link/ccip/concepts/cross-chain-tokens): Guide on handling cross-chain tokens with CCIP. -- [CCIP Mainnet Directory](https://docs.chain.link/ccip/directory/mainnet): List of supported networks on mainnet. -- [CCIP Testnet Directory](https://docs.chain.link/ccip/directory/testnet): List of supported networks on testnet. -- [CCIP Examples](https://docs.chain.link/ccip/examples): Practical examples demonstrating CCIP usage. -- [CCIP Release Notes](https://docs.chain.link/ccip/release-notes): Updates and changes in CCIP versions. -- [Supported Networks - Mainnet](https://docs.chain.link/ccip/supported-networks/v1_2_0/mainnet): Detailed information on mainnet support. -- [Supported Networks - Testnet](https://docs.chain.link/ccip/supported-networks/v1_2_0/testnet): Detailed information on testnet support. -- [CCIP Test Tokens](https://docs.chain.link/ccip/test-tokens): Information on test tokens for CCIP. -- [CCIP Tutorials](https://docs.chain.link/ccip/tutorials): Tutorials for implementing CCIP. -- [Cross-Chain Tokens Tutorial](https://docs.chain.link/ccip/tutorials/cross-chain-tokens): Tutorial on using cross-chain tokens. -- [Programmable Token Transfers](https://docs.chain.link/ccip/tutorials/programmable-token-transfers): Guide on programmable token transfers. -- [Defensive Token Transfers](https://docs.chain.link/ccip/tutorials/programmable-token-transfers-defensive): Implementing defensive token transfers. -- [Send Arbitrary Data](https://docs.chain.link/ccip/tutorials/send-arbitrary-data): Tutorial on sending arbitrary data across chains. -- [Transfer Tokens from Contract](https://docs.chain.link/ccip/tutorials/transfer-tokens-from-contract): Guide on transferring tokens from a contract. -- [USDC Transfer Tutorial](https://docs.chain.link/ccip/tutorials/usdc): Tutorial on transferring USDC using CCIP. + +* https://docs.chain.link/ccip.md +* https://docs.chain.link/ccip/getting-started.md +* https://docs.chain.link/ccip/getting-started/evm.md +* https://docs.chain.link/ccip/getting-started/aptos.md +* https://docs.chain.link/ccip/getting-started/svm.md +* https://docs.chain.link/ccip/api-reference.md +* https://docs.chain.link/ccip/concepts.md +* https://docs.chain.link/ccip/concepts/architecture.md + +### Service Data + +* https://docs.chain.link/ccip/billing.md +* https://docs.chain.link/ccip/service-limits.md + +### Directories + +* https://docs.chain.link/ccip/directory/mainnet.md +* https://docs.chain.link/ccip/directory/testnet.md + +### Tutorials + +* https://docs.chain.link/ccip/tutorials.md +* https://docs.chain.link/ccip/tutorials/evm.md +* https://docs.chain.link/ccip/tutorials/aptos.md +* https://docs.chain.link/ccip/tutorials/svm.md +* https://docs.chain.link/ccip/tutorials/ton.md ## Data Feeds -- [Data Feeds Overview](https://docs.chain.link/data-feeds): Introduction to Chainlink Data Feeds. -- [Data Feeds API Reference](https://docs.chain.link/data-feeds/api-reference): API documentation for Data Feeds. -- [Developer Responsibilities](https://docs.chain.link/data-feeds/developer-responsibilities): Guidelines for developers using Data Feeds. -- [Feed Registry](https://docs.chain.link/data-feeds/feed-registry): Information on the Feed Registry. -- [Feed Registry Functions](https://docs.chain.link/data-feeds/feed-registry/feed-registry-functions): Detailed functions of the Feed Registry. -- [Getting Started with Data Feeds](https://docs.chain.link/data-feeds/getting-started): Beginner's guide to Data Feeds. -- [Historical Data](https://docs.chain.link/data-feeds/historical-data): Accessing historical data from Data Feeds. -- [L2 Sequencer Feeds](https://docs.chain.link/data-feeds/l2-sequencer-feeds): Information on Layer 2 sequencer feeds. -- [Price Feeds](https://docs.chain.link/data-feeds/price-feeds): Overview of price feeds. -- [Price Feeds Addresses](https://docs.chain.link/data-feeds/price-feeds/addresses): Addresses for various price feeds. -- [Proof of Reserve](https://docs.chain.link/data-feeds/proof-of-reserve): Ensuring asset reserves with Chainlink. -- [Proof of Reserve Addresses](https://docs.chain.link/data-feeds/proof-of-reserve/addresses): Addresses related to Proof of Reserve. -- [Rates Feeds](https://docs.chain.link/data-feeds/rates-feeds): Information on rates feeds. -- [Rates Feeds Addresses](https://docs.chain.link/data-feeds/rates-feeds/addresses): Addresses for rates feeds. -- [Selecting Data Feeds](https://docs.chain.link/data-feeds/selecting-data-feeds): Guide to choosing appropriate data feeds. -- [SmartData](https://docs.chain.link/data-feeds/smartdata): Introduction to SmartData feeds. -- [SmartData Addresses](https://docs.chain.link/data-feeds/smartdata/addresses): Addresses for SmartData feeds. -- [Solana Data Feeds](https://docs.chain.link/data-feeds/solana): Data feeds specific to the Solana blockchain. -- [Using Data Feeds](https://docs.chain.link/data-feeds/using-data-feeds): Instructions on integrating data feeds. + +* https://docs.chain.link/data-feeds.md +* https://docs.chain.link/data-feeds/getting-started.md +* https://docs.chain.link/data-feeds/api-reference.md +* https://docs.chain.link/data-feeds/contract-registry.md + +### Addresses + +* https://docs.chain.link/data-feeds/price-feeds/addresses.md +* https://docs.chain.link/data-feeds/rates-feeds/addresses.md +* https://docs.chain.link/data-feeds/smartdata/addresses.md + ## Data Streams -- [Data Streams Overview](https://docs.chain.link/data-streams): Introduction to Chainlink Data Streams. -- [Getting Started with Data Streams](https://docs.chain.link/data-streams/getting-started): Beginner's guide to Data Streams. -- [Data Streams Billing](https://docs.chain.link/data-streams/billing): Billing information for Data Streams. -- [Stream IDs](https://docs.chain.link/data-streams/stream-ids): Understanding Stream IDs in Data Streams. + +* https://docs.chain.link/data-streams.md +* https://docs.chain.link/data-streams/architecture.md +* https://docs.chain.link/data-streams/billing.md +* https://docs.chain.link/data-streams/supported-networks.md +* https://docs.chain.link/data-streams/reference/data-streams-api.md + + +## DataLink + +* https://docs.chain.link/datalink.md +* https://docs.chain.link/datalink/billing.md +* https://docs.chain.link/datalink/pull-delivery/overview.md +* https://docs.chain.link/datalink/pull-delivery/verifier-proxy-addresses.md + ## Chainlink Automation -- [Automation Overview](https://docs.chain.link/chainlink-automation): Introduction to Chainlink Automation. -- [Getting Started with Automation](https://docs.chain.link/chainlink-automation/overview/getting-started): Guide to begin using Automation. -- [Automation Economics](https://docs.chain.link/chainlink-automation/overview/automation-economics): Economic aspects of Automation. -- [Supported Networks for Automation](https://docs.chain.link/chainlink-automation/overview/supported-networks): Networks supported by Automation. -- [Compatible Contracts](https://docs.chain.link/chainlink-automation/guides/compatible-contracts): Contracts compatible with Automation. -- [Job Scheduler Guide](https://docs.chain.link/chainlink-automation/guides/job-scheduler): Guide on scheduling jobs with Chainlink Automation. + +* https://docs.chain.link/chainlink-automation.md +* https://docs.chain.link/chainlink-automation/overview/getting-started.md +* https://docs.chain.link/chainlink-automation/overview/service-limits.md +* https://docs.chain.link/chainlink-automation/overview/supported-networks.md + ## Chainlink Functions -- [Functions Overview](https://docs.chain.link/chainlink-functions): Introduction to Chainlink Functions. -- [Getting Started with Functions](https://docs.chain.link/chainlink-functions/getting-started): Beginner's guide to Functions. -- [Functions Architecture](https://docs.chain.link/chainlink-functions/resources/architecture): Technical architecture of Functions. -- [Functions Billing](https://docs.chain.link/chainlink-functions/resources/billing): Billing information for Functions. -- [Supported Networks for Functions](https://docs.chain.link/chainlink-functions/supported-networks): Networks supported by Functions. -- [API Query Parameters Tutorial](https://docs.chain.link/chainlink-functions/tutorials/api-query-parameters): Tutorial on using API query parameters. -- [Simple Computation Tutorial](https://docs.chain.link/chainlink-functions/tutorials/simple-computation): Guide on performing simple computations using Chainlink Functions. + +* https://docs.chain.link/chainlink-functions.md +* https://docs.chain.link/chainlink-functions/getting-started.md +* https://docs.chain.link/chainlink-functions/resources/service-limits.md +* https://docs.chain.link/chainlink-functions/supported-networks.md + ## Verifiable Random Function (VRF) -- [VRF Overview](https://docs.chain.link/vrf): Introduction to Chainlink VRF. -- [Getting Started with VRF v2.5](https://docs.chain.link/vrf/v2-5/getting-started): Guide to begin using VRF v2.5. -- [VRF Billing](https://docs.chain.link/vrf/v2-5/billing): Billing details for VRF. -- [Migration from VRF v2](https://docs.chain.link/vrf/v2-5/migration-from-v2): Steps to migrate from VRF v2. -- [VRF Subscription Overview](https://docs.chain.link/vrf/v2-5/overview/subscription): Understanding VRF subscriptions. -- [Create and Manage Subscriptions](https://docs.chain.link/vrf/v2-5/subscription/create-manage): Managing VRF subscriptions. -- [Get a Random Number](https://docs.chain.link/vrf/v2-5/subscription/get-a-random-number): Guide to obtaining random numbers. -- [Supported Networks for VRF](https://docs.chain.link/vrf/v2-5/supported-networks): Networks supported by VRF. - -## Architecture -- [Decentralized Model Architecture](https://docs.chain.link/architecture-overview/architecture-decentralized-model): Overview of the decentralized architecture. -- [Architecture Overview](https://docs.chain.link/architecture-overview/architecture-overview): General architecture of Chainlink. -- [Request Model Architecture](https://docs.chain.link/architecture-overview/architecture-request-model): Understanding the request model. -- [Off-Chain Reporting](https://docs.chain.link/architecture-overview/off-chain-reporting): Details on off-chain reporting mechanisms. - -## Chainlink Nodes -- [Chainlink Nodes Overview](https://docs.chain.link/chainlink-nodes): Introduction to Chainlink nodes and their role. -- [Configuring Chainlink Nodes](https://docs.chain.link/chainlink-nodes/configuring-nodes): Guide to setting up and configuring a Chainlink node. -- [All Oracle Jobs](https://docs.chain.link/chainlink-nodes/oracle-jobs/all-jobs): Full list of supported oracle jobs. -- [All Oracle Tasks](https://docs.chain.link/chainlink-nodes/oracle-jobs/all-tasks): Detailed information on all available oracle tasks. -- [Node Requirements](https://docs.chain.link/chainlink-nodes/resources/requirements): Hardware and software requirements for running a Chainlink node. -- [Fulfilling Requests (v1)](https://docs.chain.link/chainlink-nodes/v1/fulfilling-requests): How to fulfill data requests with Chainlink v1 nodes. -- [Node Configuration (v1)](https://docs.chain.link/chainlink-nodes/v1/node-config): Best practices for configuring v1 nodes. -- [Running a Chainlink Node (v1)](https://docs.chain.link/chainlink-nodes/v1/running-a-chainlink-node): Step-by-step instructions for operating a v1 Chainlink node. + +* https://docs.chain.link/vrf.md +* https://docs.chain.link/vrf/v2-5/getting-started.md +* https://docs.chain.link/vrf/v2-5/billing.md +* https://docs.chain.link/vrf/v2-5/supported-networks.md ## Quickstarts -- [Deploy Your First Contract](https://docs.chain.link/quickstarts/deploy-your-first-contract): Beginner tutorial for deploying your first smart contract. -- [Foundry Chainlink Toolkit](https://docs.chain.link/quickstarts/foundry-chainlink-toolkit): Quickstart for using Foundry with Chainlink. -- [Historical Price Feeds API](https://docs.chain.link/quickstarts/historical-price-feeds-api): Using Chainlink APIs for historical price data. -- [Other Tutorials](https://docs.chain.link/getting-started/other-tutorials): Additional beginner-friendly tutorials. - -## Any API -- [Getting Started with Any API](https://docs.chain.link/any-api/getting-started): Guide to connecting any external API to smart contracts. -- [Any API Introduction](https://docs.chain.link/any-api/introduction): Overview of Chainlink Any API functionality. -- [Any API Reference](https://docs.chain.link/any-api/api-reference): Complete API reference for Any API connections. -- [GET Request Introduction](https://docs.chain.link/any-api/get-request/introduction): Basics of making GET requests. -- [Single Word Response Examples](https://docs.chain.link/any-api/get-request/examples/single-word-response): Example requests for fetching a single-word API response. -- [Testnet Oracles](https://docs.chain.link/any-api/testnet-oracles): Available testnet oracles for Any API usage. - -## Developer Resources -- [Chainlink Developer Certification](https://dev.chain.link/certification): Earn a certification as a Chainlink developer. -- [Conceptual Overview](https://docs.chain.link/getting-started/conceptual-overview): High-level introduction to Chainlink. -- [Acquire LINK Tokens](https://docs.chain.link/resources/acquire-link): How to obtain LINK tokens. -- [Bridge Risks](https://docs.chain.link/resources/bridge-risks): Risks associated with blockchain bridges. -- [Create a Chainlinked Project](https://docs.chain.link/resources/create-a-chainlinked-project): Building your first project using Chainlink services. -- [Developer Communications](https://docs.chain.link/resources/developer-communications): How developers can stay informed and connected. -- [Fund Your Smart Contract](https://docs.chain.link/resources/fund-your-contract): Guide to funding a contract with LINK. -- [Hackathon Resources](https://docs.chain.link/resources/hackathon-resources): Resources for participating in Chainlink hackathons. -- [LINK Token Contracts](https://docs.chain.link/resources/link-token-contracts): Contract addresses for LINK across blockchains. -- [Network Integration Resources](https://docs.chain.link/resources/network-integration): Tools and information for integrating Chainlink into various networks. - -## Utilities -- [Chainlink Faucets](https://faucets.chain.link/): Get testnet LINK, ETH and other network tokens for development. -- [PegSwap](https://pegswap.chain.link/): Convert Chainlink tokens (LINK) to be ERC-677 compatible so you can use it with Chainlink oracles. -- [Chainlink Metrics](https://metrics.chain.link/): Explore key Chainlink metrics, including network usage and ecosystem adoption. -## Economics -- [Chainlink Staking](https://staking.chain.link/): Earn rewards and support network security by staking LINK. - -## Tools -- [Chainlink Local Development Environment](https://docs.chain.link/chainlink-local): Set up a local Chainlink node environment. -- [Chainlink Functions Toolkit (NPM)](https://www.npmjs.com/package/@chainlink/functions-toolkit): NPM package for working with Chainlink Functions. -- [CCIP Tools (GitHub)](https://github.com/smartcontractkit/ccip-tools-ts): TypeScript SDK and CLI for interacting with CCIP. + +* https://docs.chain.link/quickstarts/deploy-your-first-contract.md +* https://docs.chain.link/quickstarts/chainlink-hardhat-starter-kit.md +* https://docs.chain.link/quickstarts/foundry-chainlink-toolkit.md + +## Resources + +* https://docs.chain.link/resources/getting-help.md +* https://docs.chain.link/resources/glossary.md +* https://docs.chain.link/resources/link-token-contracts.md + +## Full Documentation Bundles + +* https://docs.chain.link/cre/llms-full-go.txt +* https://docs.chain.link/cre/llms-full-ts.txt +* https://docs.chain.link/ccip/llms-full.txt +* https://docs.chain.link/data-feeds/llms-full.txt +* https://docs.chain.link/data-streams/llms-full.txt +* https://docs.chain.link/datalink/llms-full.txt +* https://docs.chain.link/chainlink-automation/llms-full.txt +* https://docs.chain.link/chainlink-functions/llms-full.txt +* https://docs.chain.link/vrf/llms-full.txt \ No newline at end of file diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts new file mode 100644 index 00000000000..52b8414a3e4 --- /dev/null +++ b/src/pages/[...path].md.ts @@ -0,0 +1,172 @@ +import type { APIRoute } from "astro" +import fs from "node:fs/promises" +import path from "node:path" +import { textPlainHeaders } from "@lib/api/cacheHeaders.js" +import { transformPageToMarkdown } from "@lib/markdown/transformMarkdown.js" +import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "@lib/markdown/utils.js" + +const SITE_BASE = "https://docs.chain.link" +const CONTENT_ROOT = path.resolve("src/content") + +const markdownHeaders = { + ...textPlainHeaders, + "Content-Type": "text/markdown; charset=utf-8", +} + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const cleanPath = normalizeMarkdownPath(params.path) + + if (!cleanPath) { + return new Response("Page not found.", { status: 404 }) + } + + const creResolution = await resolveCreCanonicalMarkdownPath(cleanPath) + + if (creResolution.kind === "selector") { + return new Response(buildCreSelectorMarkdown(cleanPath, creResolution), { + status: 200, + headers: markdownHeaders, + }) + } + + const resolvedPath = creResolution.kind === "resolved" ? creResolution.path : cleanPath + const mdxAbsPath = await findContentFile(resolvedPath) + + if (!mdxAbsPath) { + return new Response("Page not found.", { status: 404 }) + } + + const url = new URL(request.url) + const targetLanguage = url.searchParams.get("lang") || undefined + + const raw = await fs.readFile(mdxAbsPath, "utf-8") + const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) + + const section = resolvedPath.split("/")[0] + const transformed = await transformPageToMarkdown(body, mdxAbsPath, { + siteBase: SITE_BASE, + targetLanguage, + }) + + const relFromContent = toContentRelative(mdxAbsPath) + const sourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) + const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) + const lastModified = getIsoStringOrUndefined(fmLastModified) + + const headerLines = [ + `# ${title}`, + `Source: ${sourceUrl}`, + ...(lastModified ? [`Last Updated: ${lastModified}`] : []), + "", + "", + ] + + return new Response([...headerLines, transformed.trim()].join("\n"), { + status: 200, + headers: markdownHeaders, + }) +} + +function normalizeMarkdownPath(pathParam: string | undefined): string | null { + if (!pathParam) return null + + const cleanPath = pathParam.replace(/\.md$/i, "").replace(/^\/+/, "").replace(/\/+$/, "") + + if (!cleanPath) return null + + const segments = cleanPath.split("/") + if (segments.some((segment) => segment === ".." || segment === "." || segment === "")) { + return null + } + + return cleanPath +} + +async function findContentFile(cleanPath: string): Promise { + const possiblePaths = [ + path.resolve(CONTENT_ROOT, `${cleanPath}.mdx`), + path.resolve(CONTENT_ROOT, cleanPath, "index.mdx"), + path.resolve(CONTENT_ROOT, `${cleanPath}.md`), + path.resolve(CONTENT_ROOT, cleanPath, "index.md"), + ] + + for (const candidate of possiblePaths) { + if (!candidate.startsWith(`${CONTENT_ROOT}${path.sep}`)) continue + + try { + await fs.access(candidate) + return candidate + } catch { + // Try the next possible content path. + } + } + + return null +} + +type CreResolution = + | { kind: "none" } + | { kind: "resolved"; path: string } + | { kind: "selector"; goPath: string; tsPath: string } + +async function resolveCreCanonicalMarkdownPath(cleanPath: string): Promise { + if (!cleanPath.startsWith("cre/")) { + return { kind: "none" } + } + + const direct = await findContentFile(cleanPath) + if (direct) { + return { kind: "resolved", path: cleanPath } + } + + const goPath = `${cleanPath}-go` + const tsPath = `${cleanPath}-ts` + + const [goFile, tsFile] = await Promise.all([findContentFile(goPath), findContentFile(tsPath)]) + + if (goFile && tsFile) { + return { kind: "selector", goPath, tsPath } + } + + if (goFile) { + return { kind: "resolved", path: goPath } + } + + if (tsFile) { + return { kind: "resolved", path: tsPath } + } + + return { kind: "none" } +} + +function buildCreSelectorMarkdown( + canonicalPath: string, + resolution: Extract +): string { + const title = titleFromPath(canonicalPath) + const canonicalUrl = `${SITE_BASE}/${canonicalPath}` + const goUrl = `/${resolution.goPath}.md` + const tsUrl = `/${resolution.tsPath}.md` + + return [ + `# ${title}`, + `Source: ${canonicalUrl}`, + "", + "This page has language-specific markdown variants:", + "", + `- Go: ${goUrl}`, + `- TypeScript: ${tsUrl}`, + "", + ].join("\n") +} + +function titleFromPath(cleanPath: string): string { + const lastSegment = cleanPath.split("/").pop() || cleanPath + + return lastSegment + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" ") +} From b58dcb9919df3fba6a4f69e5bfc11669802f3abf Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 16:51:49 -0700 Subject: [PATCH 02/13] cre-templates --- src/pages/[...path].md.ts | 43 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index 52b8414a3e4..80451021042 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -22,6 +22,11 @@ export const GET: APIRoute = async ({ params, request }) => { return new Response("Page not found.", { status: 404 }) } + const specialResolution = await resolveSpecialCanonicalMarkdownPath(cleanPath) + if (specialResolution) { + return buildMarkdownResponseFromPath(specialResolution.resolvedPath, request, specialResolution.sourceCanonicalPath) + } + const creResolution = await resolveCreCanonicalMarkdownPath(cleanPath) if (creResolution.kind === "selector") { @@ -32,6 +37,14 @@ export const GET: APIRoute = async ({ params, request }) => { } const resolvedPath = creResolution.kind === "resolved" ? creResolution.path : cleanPath + return buildMarkdownResponseFromPath(resolvedPath, request) +} + +async function buildMarkdownResponseFromPath( + resolvedPath: string, + request: Request, + sourceCanonicalPathOverride?: string +): Promise { const mdxAbsPath = await findContentFile(resolvedPath) if (!mdxAbsPath) { @@ -51,7 +64,9 @@ export const GET: APIRoute = async ({ params, request }) => { }) const relFromContent = toContentRelative(mdxAbsPath) - const sourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) + const derivedSourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) + const sourceUrl = sourceCanonicalPathOverride ? `${SITE_BASE}/${sourceCanonicalPathOverride}` : derivedSourceUrl + const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) const lastModified = getIsoStringOrUndefined(fmLastModified) @@ -106,6 +121,32 @@ async function findContentFile(cleanPath: string): Promise { return null } +type SpecialResolution = { + resolvedPath: string + sourceCanonicalPath: string +} + +async function resolveSpecialCanonicalMarkdownPath(cleanPath: string): Promise { + const specialPathMap: Record = { + "cre-templates": "cre/templates", + } + + const mappedPath = specialPathMap[cleanPath] + if (!mappedPath) { + return null + } + + const file = await findContentFile(mappedPath) + if (!file) { + return null + } + + return { + resolvedPath: mappedPath, + sourceCanonicalPath: cleanPath, + } +} + type CreResolution = | { kind: "none" } | { kind: "resolved"; path: string } From a6477d422be427054b45ec523aba09c8cce7d7bb Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 16:58:41 -0700 Subject: [PATCH 03/13] html header --- src/layouts/DocsLayout.astro | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro index 29f092bdac0..108ce4f2985 100644 --- a/src/layouts/DocsLayout.astro +++ b/src/layouts/DocsLayout.astro @@ -19,6 +19,7 @@ interface Props { frontmatter: BaseFrontmatter headings?: MarkdownHeading[] } + const { frontmatter, headings } = Astro.props const titleHeading: MarkdownHeading = { @@ -33,9 +34,9 @@ const initialHeadings = [titleHeading].concat(filteredHeadings ?? []) const whatsNext = frontmatter.whatsnext const currentPage = new URL(Astro.request.url).pathname +const markdownUrl = `${currentPage.replace(/\/$/, "")}.md` const fileExtension = frontmatter.fileExtension || "mdx" - const baseDirectory = fileExtension === "astro" ? "src/pages" : "src/content" const currentFile = `${baseDirectory}${currentPage.replace(/\/$/, "")}${ @@ -65,7 +66,11 @@ const howToSteps = initialHeadings pageTitle={frontmatter.title} howToSteps={howToSteps} > - + + + + +
From 776be37b56a6d21b2965a8b8e2d0428e46952b15 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 20:44:18 -0700 Subject: [PATCH 04/13] replace html head with http link headers --- src/layouts/DocsLayout.astro | 9 ++------- src/middleware.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 src/middleware.ts diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro index 108ce4f2985..29f092bdac0 100644 --- a/src/layouts/DocsLayout.astro +++ b/src/layouts/DocsLayout.astro @@ -19,7 +19,6 @@ interface Props { frontmatter: BaseFrontmatter headings?: MarkdownHeading[] } - const { frontmatter, headings } = Astro.props const titleHeading: MarkdownHeading = { @@ -34,9 +33,9 @@ const initialHeadings = [titleHeading].concat(filteredHeadings ?? []) const whatsNext = frontmatter.whatsnext const currentPage = new URL(Astro.request.url).pathname -const markdownUrl = `${currentPage.replace(/\/$/, "")}.md` const fileExtension = frontmatter.fileExtension || "mdx" + const baseDirectory = fileExtension === "astro" ? "src/pages" : "src/content" const currentFile = `${baseDirectory}${currentPage.replace(/\/$/, "")}${ @@ -66,11 +65,7 @@ const howToSteps = initialHeadings pageTitle={frontmatter.title} howToSteps={howToSteps} > - - - - - +
diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 00000000000..bbb678ea6b2 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,23 @@ +import type { MiddlewareHandler } from "astro" + +export const onRequest: MiddlewareHandler = async (context, next) => { + const response = await next() + + const contentType = response.headers.get("content-type") || "" + if (!contentType.includes("text/html")) { + return response + } + + const url = new URL(context.request.url) + const pathname = url.pathname.replace(/\/$/, "") || "/" + + if (pathname.startsWith("/_astro") || pathname.includes(".")) { + return response + } + + const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` + + response.headers.append("Link", `<${markdownPath}>; rel="alternate"; type="text/markdown"`) + + return response +} From 7513abb235c817bf90eddeee7d9fe51c1a18904c Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 21:39:05 -0700 Subject: [PATCH 05/13] add middleware --- public/llms.txt | 24 +++++++++--------------- src/middleware.ts | 20 ++++++++++++-------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/public/llms.txt b/public/llms.txt index 6d442ebb1f9..c241ece87ce 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -5,9 +5,11 @@ Use this file as the root navigation index for Chainlink docs. For each task: 1. Retrieve the smallest relevant `.md` page first. -2. Prefer reference, supported networks, directory, billing, service limits, and address pages when they provide complete content. Otherwise use the closest fully rendered documentation page. -3. Do not rely on memorized addresses, supported networks, router addresses, feed addresses, chain selectors, quotas, or billing details. -4. Use product-level `llms-full.txt` files only when broad product context is required. +2. Use overview or index pages only when they contain substantive guidance or when a more specific page is not yet clear. +3. Prefer reference, supported networks, billing, service limits, and address pages only when their `.md` output contains complete rendered content. Otherwise use the closest fully rendered documentation page. +4. For CRE, use unsuffixed overview pages when they contain real conceptual or navigational guidance. Use `-go.md` and `-ts.md` pages when the implementation content is language-specific. +5. Do not rely on memorized addresses, supported networks, router addresses, feed addresses, chain selectors, quotas, or billing details. +6. Use product-level `llms-full.txt` files only when broad product context is required. Current defaults: @@ -108,11 +110,6 @@ Current defaults: * https://docs.chain.link/ccip/billing.md * https://docs.chain.link/ccip/service-limits.md -### Directories - -* https://docs.chain.link/ccip/directory/mainnet.md -* https://docs.chain.link/ccip/directory/testnet.md - ### Tutorials * https://docs.chain.link/ccip/tutorials.md @@ -121,6 +118,7 @@ Current defaults: * https://docs.chain.link/ccip/tutorials/svm.md * https://docs.chain.link/ccip/tutorials/ton.md + ## Data Feeds * https://docs.chain.link/data-feeds.md @@ -128,12 +126,6 @@ Current defaults: * https://docs.chain.link/data-feeds/api-reference.md * https://docs.chain.link/data-feeds/contract-registry.md -### Addresses - -* https://docs.chain.link/data-feeds/price-feeds/addresses.md -* https://docs.chain.link/data-feeds/rates-feeds/addresses.md -* https://docs.chain.link/data-feeds/smartdata/addresses.md - ## Data Streams @@ -149,7 +141,6 @@ Current defaults: * https://docs.chain.link/datalink.md * https://docs.chain.link/datalink/billing.md * https://docs.chain.link/datalink/pull-delivery/overview.md -* https://docs.chain.link/datalink/pull-delivery/verifier-proxy-addresses.md ## Chainlink Automation @@ -175,18 +166,21 @@ Current defaults: * https://docs.chain.link/vrf/v2-5/billing.md * https://docs.chain.link/vrf/v2-5/supported-networks.md + ## Quickstarts * https://docs.chain.link/quickstarts/deploy-your-first-contract.md * https://docs.chain.link/quickstarts/chainlink-hardhat-starter-kit.md * https://docs.chain.link/quickstarts/foundry-chainlink-toolkit.md + ## Resources * https://docs.chain.link/resources/getting-help.md * https://docs.chain.link/resources/glossary.md * https://docs.chain.link/resources/link-token-contracts.md + ## Full Documentation Bundles * https://docs.chain.link/cre/llms-full-go.txt diff --git a/src/middleware.ts b/src/middleware.ts index bbb678ea6b2..1dae52a579f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,22 +1,26 @@ import type { MiddlewareHandler } from "astro" export const onRequest: MiddlewareHandler = async (context, next) => { - const response = await next() + const url = context.url + const pathname = url.pathname.replace(/\/$/, "") || "/" + const accept = context.request.headers.get("accept") || "" - const contentType = response.headers.get("content-type") || "" - if (!contentType.includes("text/html")) { - return response + const isAssetLike = pathname.startsWith("/_astro") || pathname.includes(".") + const wantsMarkdown = accept.toLowerCase().includes("text/markdown") && !pathname.endsWith(".md") && !isAssetLike + + if (wantsMarkdown) { + const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` + return context.rewrite(markdownPath) } - const url = new URL(context.request.url) - const pathname = url.pathname.replace(/\/$/, "") || "/" + const response = await next() - if (pathname.startsWith("/_astro") || pathname.includes(".")) { + const contentType = response.headers.get("content-type") || "" + if (!contentType.includes("text/html") || isAssetLike) { return response } const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` - response.headers.append("Link", `<${markdownPath}>; rel="alternate"; type="text/markdown"`) return response From 78a0c2abd0fcbbee4107ea653eb2b01590c422e3 Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 21:49:08 -0700 Subject: [PATCH 06/13] update middleware --- src/middleware.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/middleware.ts b/src/middleware.ts index 1dae52a579f..c8bcfdd66ab 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -15,6 +15,8 @@ export const onRequest: MiddlewareHandler = async (context, next) => { const response = await next() + response.headers.append("Vary", "Accept") + const contentType = response.headers.get("content-type") || "" if (!contentType.includes("text/html") || isAssetLike) { return response From 3dc5b919250ab95b8a8588d3105d57158b3c461d Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Wed, 29 Apr 2026 22:01:12 -0700 Subject: [PATCH 07/13] middleware w/o accept --- src/middleware.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index c8bcfdd66ab..9e44d1e656a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,26 +3,18 @@ import type { MiddlewareHandler } from "astro" export const onRequest: MiddlewareHandler = async (context, next) => { const url = context.url const pathname = url.pathname.replace(/\/$/, "") || "/" - const accept = context.request.headers.get("accept") || "" const isAssetLike = pathname.startsWith("/_astro") || pathname.includes(".") - const wantsMarkdown = accept.toLowerCase().includes("text/markdown") && !pathname.endsWith(".md") && !isAssetLike - - if (wantsMarkdown) { - const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` - return context.rewrite(markdownPath) - } const response = await next() - response.headers.append("Vary", "Accept") - const contentType = response.headers.get("content-type") || "" if (!contentType.includes("text/html") || isAssetLike) { return response } const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` + response.headers.append("Link", `<${markdownPath}>; rel="alternate"; type="text/markdown"`) return response From e40126e95307459b95ae4b4d2f1da37cdfcbdb5c Mon Sep 17 00:00:00 2001 From: Grace Fletcher Date: Thu, 30 Apr 2026 14:02:08 -0700 Subject: [PATCH 08/13] md endpoints + accept + discoverability --- astro.config.ts | 9 +- src/lib/markdown/buildMarkdownResponse.ts | 241 +++++++++++++++++ src/middleware.ts | 24 +- src/pages/[...path].md.ts | 208 +-------------- src/pages/ace/[...id].astro | 51 +++- src/pages/ace/[...id].md.ts | 20 ++ src/pages/ace/index.astro | 34 ++- src/pages/any-api/[...id].astro | 51 +++- src/pages/any-api/[...id].md.ts | 20 ++ src/pages/architecture-overview/[...id].astro | 51 +++- src/pages/architecture-overview/[...id].md.ts | 20 ++ src/pages/ccip.md.ts | 17 ++ src/pages/ccip/[...id].astro | 96 ++++--- src/pages/ccip/[...id].md.ts | 20 ++ src/pages/ccip/index.astro | 35 ++- src/pages/ccip/tutorials/[...id].astro | 51 +++- src/pages/ccip/tutorials/[...id].md.ts | 20 ++ src/pages/chainlink-automation/[...id].astro | 51 +++- src/pages/chainlink-automation/[...id].md.ts | 20 ++ src/pages/chainlink-automation/index.astro | 35 ++- src/pages/chainlink-functions/[...id].astro | 51 +++- src/pages/chainlink-functions/[...id].md.ts | 20 ++ src/pages/chainlink-functions/index.astro | 35 ++- src/pages/chainlink-local/[...id].astro | 51 +++- src/pages/chainlink-local/[...id].md.ts | 20 ++ src/pages/chainlink-local/index.astro | 35 ++- src/pages/chainlink-nodes/[...id].astro | 51 +++- src/pages/chainlink-nodes/[...id].md.ts | 20 ++ src/pages/cre-templates.md.ts | 17 ++ src/pages/cre-templates/[...id].astro | 51 +++- src/pages/cre-templates/[...id].md.ts | 20 ++ src/pages/cre-templates/index.astro | 32 +++ src/pages/cre/[...id].astro | 242 ++++++++++-------- src/pages/cre/[...id].md.ts | 20 ++ src/pages/cre/index.astro | 34 ++- src/pages/data-feeds/[...id].astro | 51 +++- src/pages/data-feeds/[...id].md.ts | 20 ++ src/pages/data-feeds/index.astro | 35 ++- src/pages/data-streams/[...id].astro | 51 +++- src/pages/data-streams/[...id].md.ts | 20 ++ src/pages/data-streams/index.astro | 35 ++- src/pages/datalink/[...id].astro | 51 +++- src/pages/datalink/[...id].md.ts | 20 ++ src/pages/datalink/index.astro | 35 ++- .../dta-technical-standard/[...id].astro | 51 +++- .../dta-technical-standard/[...id].md.ts | 20 ++ src/pages/dta-technical-standard/index.astro | 35 ++- src/pages/getting-started/[...id].astro | 51 +++- src/pages/getting-started/[...id].md.ts | 20 ++ src/pages/oracle-platform/[...id].astro | 51 +++- src/pages/oracle-platform/[...id].md.ts | 20 ++ src/pages/resources/[...id].astro | 51 +++- src/pages/resources/[...id].md.ts | 20 ++ src/pages/vrf/[...id].astro | 48 +++- src/pages/vrf/[...id].md.ts | 26 ++ src/pages/vrf/index.astro | 35 ++- 56 files changed, 1923 insertions(+), 596 deletions(-) create mode 100644 src/lib/markdown/buildMarkdownResponse.ts create mode 100644 src/pages/ace/[...id].md.ts create mode 100644 src/pages/any-api/[...id].md.ts create mode 100644 src/pages/architecture-overview/[...id].md.ts create mode 100644 src/pages/ccip.md.ts create mode 100644 src/pages/ccip/[...id].md.ts create mode 100644 src/pages/ccip/tutorials/[...id].md.ts create mode 100644 src/pages/chainlink-automation/[...id].md.ts create mode 100644 src/pages/chainlink-functions/[...id].md.ts create mode 100644 src/pages/chainlink-local/[...id].md.ts create mode 100644 src/pages/chainlink-nodes/[...id].md.ts create mode 100644 src/pages/cre-templates.md.ts create mode 100644 src/pages/cre-templates/[...id].md.ts create mode 100644 src/pages/cre/[...id].md.ts create mode 100644 src/pages/data-feeds/[...id].md.ts create mode 100644 src/pages/data-streams/[...id].md.ts create mode 100644 src/pages/datalink/[...id].md.ts create mode 100644 src/pages/dta-technical-standard/[...id].md.ts create mode 100644 src/pages/getting-started/[...id].md.ts create mode 100644 src/pages/oracle-platform/[...id].md.ts create mode 100644 src/pages/resources/[...id].md.ts create mode 100644 src/pages/vrf/[...id].md.ts diff --git a/astro.config.ts b/astro.config.ts index 576180d559a..ac8665a036e 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -50,10 +50,11 @@ export default defineConfig({ integrations: [ trailingSlashMiddleware(), preact({ - include: ["**/preact/*"], + include: ["**/preact/**"], }), react({ - include: ["**/react/*"], + include: ["**/*.tsx", "**/*.jsx"], + exclude: ["**/preact/**"], }), sitemap({ changefreq: "daily", @@ -130,7 +131,9 @@ export default defineConfig({ }, // output: 'static' (fully static or partial SSR with `prerender = false` ==> export const prerender = false;) output: "static", - adapter: vercel(), + adapter: vercel({ + edgeMiddleware: true, + }), vite: { plugins: [yaml()], build: { diff --git a/src/lib/markdown/buildMarkdownResponse.ts b/src/lib/markdown/buildMarkdownResponse.ts new file mode 100644 index 00000000000..ae4359066ce --- /dev/null +++ b/src/lib/markdown/buildMarkdownResponse.ts @@ -0,0 +1,241 @@ +import fs from "node:fs/promises" +import path from "node:path" +import { textPlainHeaders } from "@lib/api/cacheHeaders.js" +import { transformPageToMarkdown } from "@lib/markdown/transformMarkdown.js" +import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "@lib/markdown/utils.js" + +const SITE_BASE = "https://docs.chain.link" +const CONTENT_ROOT = path.resolve("src/content") + +export const markdownHeaders = { + ...textPlainHeaders, + "Content-Type": "text/markdown; charset=utf-8", +} + +export function normalizeMarkdownPath(pathParam: string | undefined): string | null { + if (!pathParam) return null + + const cleanPath = pathParam.replace(/\.md$/i, "").replace(/^\/+/, "").replace(/\/+$/, "") + + if (!cleanPath) return null + + const segments = cleanPath.split("/") + if (segments.some((segment) => segment === ".." || segment === "." || segment === "")) { + return null + } + + return cleanPath +} + +export async function findContentFile(cleanPath: string): Promise { + const possiblePaths = [ + path.resolve(CONTENT_ROOT, `${cleanPath}.mdx`), + path.resolve(CONTENT_ROOT, cleanPath, "index.mdx"), + path.resolve(CONTENT_ROOT, `${cleanPath}.md`), + path.resolve(CONTENT_ROOT, cleanPath, "index.md"), + ] + + for (const candidate of possiblePaths) { + if (!candidate.startsWith(`${CONTENT_ROOT}${path.sep}`)) continue + + try { + await fs.access(candidate) + return candidate + } catch { + // Try the next possible content path. + } + } + + return null +} + +export type SpecialResolution = { + resolvedPath: string + sourceCanonicalPath: string +} + +export async function resolveSpecialCanonicalMarkdownPath(cleanPath: string): Promise { + const specialPathMap: Record = { + "cre-templates": "cre/templates", + } + + const mappedPath = specialPathMap[cleanPath] + if (!mappedPath) { + return null + } + + const file = await findContentFile(mappedPath) + if (!file) { + return null + } + + return { + resolvedPath: mappedPath, + sourceCanonicalPath: cleanPath, + } +} + +export type CreResolution = + | { kind: "none" } + | { kind: "resolved"; path: string } + | { kind: "selector"; goPath: string; tsPath: string } + +export async function resolveCreCanonicalMarkdownPath(cleanPath: string): Promise { + if (!cleanPath.startsWith("cre/")) { + return { kind: "none" } + } + + const direct = await findContentFile(cleanPath) + if (direct) { + return { kind: "resolved", path: cleanPath } + } + + const goPath = `${cleanPath}-go` + const tsPath = `${cleanPath}-ts` + + const [goFile, tsFile] = await Promise.all([findContentFile(goPath), findContentFile(tsPath)]) + + if (goFile && tsFile) { + return { kind: "selector", goPath, tsPath } + } + + if (goFile) { + return { kind: "resolved", path: goPath } + } + + if (tsFile) { + return { kind: "resolved", path: tsPath } + } + + return { kind: "none" } +} + +export function buildCreSelectorMarkdown( + canonicalPath: string, + resolution: Extract +): string { + const title = titleFromPath(canonicalPath) + const canonicalUrl = `${SITE_BASE}/${canonicalPath}` + const goUrl = `/${resolution.goPath}.md` + const tsUrl = `/${resolution.tsPath}.md` + + return [ + `# ${title}`, + `Source: ${canonicalUrl}`, + "", + "This page has language-specific markdown variants:", + "", + `- Go: ${goUrl}`, + `- TypeScript: ${tsUrl}`, + "", + ].join("\n") +} + +export function titleFromPath(cleanPath: string): string { + const lastSegment = cleanPath.split("/").pop() || cleanPath + + return lastSegment + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" ") +} + +export type MarkdownTarget = + | { kind: "not-found" } + | { kind: "selector"; markdown: string } + | { kind: "resolved"; resolvedPath: string; sourceCanonicalPathOverride?: string } + +export async function resolveMarkdownTarget(cleanPath: string): Promise { + const specialResolution = await resolveSpecialCanonicalMarkdownPath(cleanPath) + if (specialResolution) { + return { + kind: "resolved", + resolvedPath: specialResolution.resolvedPath, + sourceCanonicalPathOverride: specialResolution.sourceCanonicalPath, + } + } + + const creResolution = await resolveCreCanonicalMarkdownPath(cleanPath) + + if (creResolution.kind === "selector") { + return { + kind: "selector", + markdown: buildCreSelectorMarkdown(cleanPath, creResolution), + } + } + + if (creResolution.kind === "resolved") { + return { + kind: "resolved", + resolvedPath: creResolution.path, + } + } + + const file = await findContentFile(cleanPath) + if (file) { + return { + kind: "resolved", + resolvedPath: cleanPath, + } + } + + return { kind: "not-found" } +} + +export async function buildMarkdownDocumentFromPath( + resolvedPath: string, + request: Request, + sourceCanonicalPathOverride?: string +): Promise { + const mdxAbsPath = await findContentFile(resolvedPath) + + if (!mdxAbsPath) { + return null + } + + const url = new URL(request.url) + const targetLanguage = url.searchParams.get("lang") || undefined + + const raw = await fs.readFile(mdxAbsPath, "utf-8") + const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) + + const section = resolvedPath.split("/")[0] + const transformed = await transformPageToMarkdown(body, mdxAbsPath, { + siteBase: SITE_BASE, + targetLanguage, + }) + + const relFromContent = toContentRelative(mdxAbsPath) + const derivedSourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) + const sourceUrl = sourceCanonicalPathOverride ? `${SITE_BASE}/${sourceCanonicalPathOverride}` : derivedSourceUrl + + const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) + const lastModified = getIsoStringOrUndefined(fmLastModified) + + const headerLines = [ + `# ${title}`, + `Source: ${sourceUrl}`, + ...(lastModified ? [`Last Updated: ${lastModified}`] : []), + "", + "", + ] + + return [...headerLines, transformed.trim()].join("\n") +} + +export async function buildMarkdownResponseFromPath( + resolvedPath: string, + request: Request, + sourceCanonicalPathOverride?: string +): Promise { + const markdown = await buildMarkdownDocumentFromPath(resolvedPath, request, sourceCanonicalPathOverride) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/middleware.ts b/src/middleware.ts index 9e44d1e656a..08e9f7317c1 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,21 +1,5 @@ -import type { MiddlewareHandler } from "astro" +import { defineMiddleware } from "astro:middleware" -export const onRequest: MiddlewareHandler = async (context, next) => { - const url = context.url - const pathname = url.pathname.replace(/\/$/, "") || "/" - - const isAssetLike = pathname.startsWith("/_astro") || pathname.includes(".") - - const response = await next() - - const contentType = response.headers.get("content-type") || "" - if (!contentType.includes("text/html") || isAssetLike) { - return response - } - - const markdownPath = pathname === "/" ? "/index.md" : `${pathname}.md` - - response.headers.append("Link", `<${markdownPath}>; rel="alternate"; type="text/markdown"`) - - return response -} +export const onRequest = defineMiddleware(async (_context, next) => { + return next() +}) diff --git a/src/pages/[...path].md.ts b/src/pages/[...path].md.ts index 80451021042..e8a31861912 100644 --- a/src/pages/[...path].md.ts +++ b/src/pages/[...path].md.ts @@ -1,213 +1,33 @@ import type { APIRoute } from "astro" -import fs from "node:fs/promises" -import path from "node:path" -import { textPlainHeaders } from "@lib/api/cacheHeaders.js" -import { transformPageToMarkdown } from "@lib/markdown/transformMarkdown.js" -import { extractFrontmatter, getIsoStringOrUndefined, toCanonicalUrl, toContentRelative } from "@lib/markdown/utils.js" - -const SITE_BASE = "https://docs.chain.link" -const CONTENT_ROOT = path.resolve("src/content") - -const markdownHeaders = { - ...textPlainHeaders, - "Content-Type": "text/markdown; charset=utf-8", -} +import { + buildMarkdownResponseFromPath, + markdownHeaders, + normalizeMarkdownPath, + resolveMarkdownTarget, +} from "@lib/markdown/buildMarkdownResponse.js" export const prerender = false export const GET: APIRoute = async ({ params, request }) => { + console.log("[global md catch-all hit]", params.path) const cleanPath = normalizeMarkdownPath(params.path) if (!cleanPath) { return new Response("Page not found.", { status: 404 }) } - const specialResolution = await resolveSpecialCanonicalMarkdownPath(cleanPath) - if (specialResolution) { - return buildMarkdownResponseFromPath(specialResolution.resolvedPath, request, specialResolution.sourceCanonicalPath) - } + const target = await resolveMarkdownTarget(cleanPath) - const creResolution = await resolveCreCanonicalMarkdownPath(cleanPath) + if (target.kind === "not-found") { + return new Response("Page not found.", { status: 404 }) + } - if (creResolution.kind === "selector") { - return new Response(buildCreSelectorMarkdown(cleanPath, creResolution), { + if (target.kind === "selector") { + return new Response(target.markdown, { status: 200, headers: markdownHeaders, }) } - const resolvedPath = creResolution.kind === "resolved" ? creResolution.path : cleanPath - return buildMarkdownResponseFromPath(resolvedPath, request) -} - -async function buildMarkdownResponseFromPath( - resolvedPath: string, - request: Request, - sourceCanonicalPathOverride?: string -): Promise { - const mdxAbsPath = await findContentFile(resolvedPath) - - if (!mdxAbsPath) { - return new Response("Page not found.", { status: 404 }) - } - - const url = new URL(request.url) - const targetLanguage = url.searchParams.get("lang") || undefined - - const raw = await fs.readFile(mdxAbsPath, "utf-8") - const { body, fmTitle, fmLastModified } = extractFrontmatter(raw) - - const section = resolvedPath.split("/")[0] - const transformed = await transformPageToMarkdown(body, mdxAbsPath, { - siteBase: SITE_BASE, - targetLanguage, - }) - - const relFromContent = toContentRelative(mdxAbsPath) - const derivedSourceUrl = toCanonicalUrl(section, relFromContent, SITE_BASE) - const sourceUrl = sourceCanonicalPathOverride ? `${SITE_BASE}/${sourceCanonicalPathOverride}` : derivedSourceUrl - - const title = fmTitle || path.basename(mdxAbsPath, path.extname(mdxAbsPath)) - const lastModified = getIsoStringOrUndefined(fmLastModified) - - const headerLines = [ - `# ${title}`, - `Source: ${sourceUrl}`, - ...(lastModified ? [`Last Updated: ${lastModified}`] : []), - "", - "", - ] - - return new Response([...headerLines, transformed.trim()].join("\n"), { - status: 200, - headers: markdownHeaders, - }) -} - -function normalizeMarkdownPath(pathParam: string | undefined): string | null { - if (!pathParam) return null - - const cleanPath = pathParam.replace(/\.md$/i, "").replace(/^\/+/, "").replace(/\/+$/, "") - - if (!cleanPath) return null - - const segments = cleanPath.split("/") - if (segments.some((segment) => segment === ".." || segment === "." || segment === "")) { - return null - } - - return cleanPath -} - -async function findContentFile(cleanPath: string): Promise { - const possiblePaths = [ - path.resolve(CONTENT_ROOT, `${cleanPath}.mdx`), - path.resolve(CONTENT_ROOT, cleanPath, "index.mdx"), - path.resolve(CONTENT_ROOT, `${cleanPath}.md`), - path.resolve(CONTENT_ROOT, cleanPath, "index.md"), - ] - - for (const candidate of possiblePaths) { - if (!candidate.startsWith(`${CONTENT_ROOT}${path.sep}`)) continue - - try { - await fs.access(candidate) - return candidate - } catch { - // Try the next possible content path. - } - } - - return null -} - -type SpecialResolution = { - resolvedPath: string - sourceCanonicalPath: string -} - -async function resolveSpecialCanonicalMarkdownPath(cleanPath: string): Promise { - const specialPathMap: Record = { - "cre-templates": "cre/templates", - } - - const mappedPath = specialPathMap[cleanPath] - if (!mappedPath) { - return null - } - - const file = await findContentFile(mappedPath) - if (!file) { - return null - } - - return { - resolvedPath: mappedPath, - sourceCanonicalPath: cleanPath, - } -} - -type CreResolution = - | { kind: "none" } - | { kind: "resolved"; path: string } - | { kind: "selector"; goPath: string; tsPath: string } - -async function resolveCreCanonicalMarkdownPath(cleanPath: string): Promise { - if (!cleanPath.startsWith("cre/")) { - return { kind: "none" } - } - - const direct = await findContentFile(cleanPath) - if (direct) { - return { kind: "resolved", path: cleanPath } - } - - const goPath = `${cleanPath}-go` - const tsPath = `${cleanPath}-ts` - - const [goFile, tsFile] = await Promise.all([findContentFile(goPath), findContentFile(tsPath)]) - - if (goFile && tsFile) { - return { kind: "selector", goPath, tsPath } - } - - if (goFile) { - return { kind: "resolved", path: goPath } - } - - if (tsFile) { - return { kind: "resolved", path: tsPath } - } - - return { kind: "none" } -} - -function buildCreSelectorMarkdown( - canonicalPath: string, - resolution: Extract -): string { - const title = titleFromPath(canonicalPath) - const canonicalUrl = `${SITE_BASE}/${canonicalPath}` - const goUrl = `/${resolution.goPath}.md` - const tsUrl = `/${resolution.tsPath}.md` - - return [ - `# ${title}`, - `Source: ${canonicalUrl}`, - "", - "This page has language-specific markdown variants:", - "", - `- Go: ${goUrl}`, - `- TypeScript: ${tsUrl}`, - "", - ].join("\n") -} - -function titleFromPath(cleanPath: string): string { - const lastSegment = cleanPath.split("/").pop() || cleanPath - - return lastSegment - .split("-") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(" ") + return buildMarkdownResponseFromPath(target.resolvedPath, request, target.sourceCanonicalPathOverride) } diff --git a/src/pages/ace/[...id].astro b/src/pages/ace/[...id].astro index a9ce6bfc58a..1e7f6c2debd 100644 --- a/src/pages/ace/[...id].astro +++ b/src/pages/ace/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const aceEntries = await getCollection("ace") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return aceEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("ace") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "ace" : `ace/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/ace/[...id].md.ts b/src/pages/ace/[...id].md.ts new file mode 100644 index 00000000000..5cfdaea6f2a --- /dev/null +++ b/src/pages/ace/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "ace" : `ace/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/ace/index.astro b/src/pages/ace/index.astro index 9e53e7fe3b7..feba6a4995e 100644 --- a/src/pages/ace/index.astro +++ b/src/pages/ace/index.astro @@ -1,13 +1,45 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getEntry, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const entry = await getEntry("ace", "index") + if (!entry) { - throw new Error("No entry found for ACE index!") + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "ace" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/any-api/[...id].astro b/src/pages/any-api/[...id].astro index 9c2990c8cac..cebcd8379be 100644 --- a/src/pages/any-api/[...id].astro +++ b/src/pages/any-api/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const anyApiEntries = await getCollection("any-api") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return anyApiEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("any-api") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "any-api" : `any-api/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/any-api/[...id].md.ts b/src/pages/any-api/[...id].md.ts new file mode 100644 index 00000000000..f4e331b8c8f --- /dev/null +++ b/src/pages/any-api/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "any-api" : `any-api/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/architecture-overview/[...id].astro b/src/pages/architecture-overview/[...id].astro index 5d056955d20..24ac77d5df6 100644 --- a/src/pages/architecture-overview/[...id].astro +++ b/src/pages/architecture-overview/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const architectureOverviewEntries = await getCollection("architecture-overview") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return architectureOverviewEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("architecture-overview") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "architecture-overview" : `architecture-overview/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/architecture-overview/[...id].md.ts b/src/pages/architecture-overview/[...id].md.ts new file mode 100644 index 00000000000..d27477eee87 --- /dev/null +++ b/src/pages/architecture-overview/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "architecture-overview" : `architecture-overview/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/ccip.md.ts b/src/pages/ccip.md.ts new file mode 100644 index 00000000000..787a99eaa79 --- /dev/null +++ b/src/pages/ccip.md.ts @@ -0,0 +1,17 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ request }) => { + const markdown = await buildMarkdownDocumentFromPath("ccip", request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/ccip/[...id].astro b/src/pages/ccip/[...id].astro index fb6bfc9b6c6..2742593e54d 100644 --- a/src/pages/ccip/[...id].astro +++ b/src/pages/ccip/[...id].astro @@ -1,56 +1,78 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const ccipEntries = await getCollection("ccip") - const pathMap = new Map() +function mapCcipRouteId(entryId: string): string | null { + const urlPath = entryId.replace(/\.(md|mdx)$/, "") - // Process all entries to build URL path mapping - for (const entry of ccipEntries) { - const urlPath = entry.id.replace(/\.(md|mdx)$/, "") + // Skip directory and tutorial entries (handled by dedicated routes) + if (urlPath.startsWith("directory/") || urlPath.startsWith("tutorials/")) { + return null + } - // Skip directory and tutorial entries (handled by dedicated routes) - if (urlPath.startsWith("directory/") || urlPath.startsWith("tutorials/")) { - continue - } + // Handle API reference version format conversion (v150 -> v1.5.0) + if (urlPath.includes("/api-reference/") && urlPath.includes("/v")) { + const parts = urlPath.split("/") + const versionIndex = parts.findIndex((part) => part.startsWith("v") && /^v\d+$/.test(part)) - // Handle API reference version format conversion (if needed) - if (urlPath.includes("/api-reference/") && urlPath.includes("/v")) { - const parts = urlPath.split("/") - const versionIndex = parts.findIndex((part) => part.startsWith("v") && /^v\d+$/.test(part)) - - if (versionIndex !== -1) { - const versionPart = parts[versionIndex] - // Convert version format if needed (v150 → v1.5.0) - if (versionPart.length === 4) { - const formattedVersion = `v${versionPart[1]}.${versionPart[2]}.${versionPart[3]}` - parts[versionIndex] = formattedVersion - pathMap.set(parts.join("/"), entry) - } else { - pathMap.set(urlPath, entry) - } - } else { - pathMap.set(urlPath, entry) + if (versionIndex !== -1) { + const versionPart = parts[versionIndex] + if (versionPart.length === 4) { + const formattedVersion = `v${versionPart[1]}.${versionPart[2]}.${versionPart[3]}` + parts[versionIndex] = formattedVersion + return parts.join("/") } - } else { - pathMap.set(urlPath, entry) } } - // Generate static paths - return Array.from(pathMap.entries()).map(([urlPath, entry]) => ({ - params: { id: urlPath }, - props: { entry }, - })) + return urlPath +} + +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" + +const ccipEntries = await getCollection("ccip") + +const entry = ccipEntries.find((candidate) => { + const mapped = mapCcipRouteId(candidate.id) + return mapped === requestedId +}) + +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -interface Props { - entry: Awaited>>[number] +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "ccip" : `ccip/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -const { entry } = Astro.props const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/ccip/[...id].md.ts b/src/pages/ccip/[...id].md.ts new file mode 100644 index 00000000000..ebbb9ec5cbd --- /dev/null +++ b/src/pages/ccip/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "ccip" : `ccip/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/ccip/index.astro b/src/pages/ccip/index.astro index 816caada5a7..1ad17fa66b0 100644 --- a/src/pages/ccip/index.astro +++ b/src/pages/ccip/index.astro @@ -1,14 +1,45 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getEntry, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const entry = await getEntry("ccip", "index") + if (!entry) { - throw new Error("No entry found for ccip index!") + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "ccip" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -// use the global `render()` function const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/ccip/tutorials/[...id].astro b/src/pages/ccip/tutorials/[...id].astro index 2f8701cb76e..c230fdba25c 100644 --- a/src/pages/ccip/tutorials/[...id].astro +++ b/src/pages/ccip/tutorials/[...id].astro @@ -1,35 +1,60 @@ --- +export const prerender = false + import { getCollection, render, type CollectionEntry } from "astro:content" import TutorialLayout from "../../../layouts/TutorialLayout.astro" import DocsLayout from "../../../layouts/DocsLayout.astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" type Props = { entry: CollectionEntry<"ccip"> } -export async function getStaticPaths() { - const entries = await getCollection("ccip", (entry) => entry.id.startsWith("tutorials/")) +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" + +const entries = await getCollection("ccip", (entry) => entry.id.startsWith("tutorials/")) + +const entry = entries.find((candidate) => { + const tutorialId = candidate.id.replace(/^tutorials\//, "").replace(/\.(md|mdx)$/, "") + return tutorialId === requestedId +}) + +if (!entry) { + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") - const paths: { params: { id: string }; props: Props }[] = [] +const canonicalPath = requestedId === "index" ? "ccip/tutorials" : `ccip/tutorials/${requestedId}` - for (const entry of entries) { - const tutorialId = entry.id.replace(/^tutorials\//, "").replace(/\.(md|mdx)$/, "") +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) - paths.push({ - params: { id: tutorialId }, - props: { entry }, - }) + if (!markdown) { + return new Response("Page not found.", { status: 404 }) } - return paths + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -const { entry } = Astro.props as Props - const { Content, headings } = await render(entry) -// Only use TutorialLayout for specific interactive tutorials +// Preserve original layout switch const Layout = entry.id === "tutorials/evm/cross-chain-tokens/register-from-eoa-remix" ? TutorialLayout : DocsLayout + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/ccip/tutorials/[...id].md.ts b/src/pages/ccip/tutorials/[...id].md.ts new file mode 100644 index 00000000000..de2979c4e92 --- /dev/null +++ b/src/pages/ccip/tutorials/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "ccip/tutorials" : `ccip/tutorials/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/chainlink-automation/[...id].astro b/src/pages/chainlink-automation/[...id].astro index c43a315e14d..24959a7d3c1 100644 --- a/src/pages/chainlink-automation/[...id].astro +++ b/src/pages/chainlink-automation/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const chainlinkAutomationEntries = await getCollection("chainlink-automation") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return chainlinkAutomationEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("chainlink-automation") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "" : `/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-automation/[...id].md.ts b/src/pages/chainlink-automation/[...id].md.ts new file mode 100644 index 00000000000..b3a08ac8d0d --- /dev/null +++ b/src/pages/chainlink-automation/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "chainlink-automation" : `chainlink-automation/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/chainlink-automation/index.astro b/src/pages/chainlink-automation/index.astro index c70818d8d2c..9e946f5dc51 100644 --- a/src/pages/chainlink-automation/index.astro +++ b/src/pages/chainlink-automation/index.astro @@ -1,14 +1,45 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getEntry, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const entry = await getEntry("chainlink-automation", "index") + if (!entry) { - throw new Error("No entry found for chainlink-automation index!") + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "chainlink-automation" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -// use the global `render()` function const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-functions/[...id].astro b/src/pages/chainlink-functions/[...id].astro index 90b615f8e88..3d0c9ef8e93 100644 --- a/src/pages/chainlink-functions/[...id].astro +++ b/src/pages/chainlink-functions/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const chainlinkFunctionsEntries = await getCollection("chainlink-functions") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return chainlinkFunctionsEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("chainlink-functions") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "chainlink-functions" : `chainlink-functions/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-functions/[...id].md.ts b/src/pages/chainlink-functions/[...id].md.ts new file mode 100644 index 00000000000..aa4bfc1589f --- /dev/null +++ b/src/pages/chainlink-functions/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "chainlink-functions" : `chainlink-functions/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/chainlink-functions/index.astro b/src/pages/chainlink-functions/index.astro index d0bcc72a826..9f154145dc2 100644 --- a/src/pages/chainlink-functions/index.astro +++ b/src/pages/chainlink-functions/index.astro @@ -1,14 +1,45 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getEntry, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const entry = await getEntry("chainlink-functions", "index") + if (!entry) { - throw new Error("No entry found for chainlink-functions index!") + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "chainlink-functions" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -// use the global `render()` function const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-local/[...id].astro b/src/pages/chainlink-local/[...id].astro index ac32e10d111..bb9a52b91b7 100644 --- a/src/pages/chainlink-local/[...id].astro +++ b/src/pages/chainlink-local/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const chainlinkLocalEntries = await getCollection("chainlink-local") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return chainlinkLocalEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("chainlink-local") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "chainlink-local" : `chainlink-local/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-local/[...id].md.ts b/src/pages/chainlink-local/[...id].md.ts new file mode 100644 index 00000000000..f3f13199f89 --- /dev/null +++ b/src/pages/chainlink-local/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "chainlink-local" : `chainlink-local/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/chainlink-local/index.astro b/src/pages/chainlink-local/index.astro index c7b477c589f..ed96ff01423 100644 --- a/src/pages/chainlink-local/index.astro +++ b/src/pages/chainlink-local/index.astro @@ -1,14 +1,45 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getEntry, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const entry = await getEntry("chainlink-local", "index") + if (!entry) { - throw new Error("No entry found for chainlink-local index!") + return new Response("Page not found.", { status: 404 }) +} + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } -// use the global `render()` function const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-nodes/[...id].astro b/src/pages/chainlink-nodes/[...id].astro index 11bc878b123..a16032cfc75 100644 --- a/src/pages/chainlink-nodes/[...id].astro +++ b/src/pages/chainlink-nodes/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import DocsLayout from "~/layouts/DocsLayout.astro" import { getCollection, render } from "astro:content" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const chainlinkNodesEntries = await getCollection("chainlink-nodes") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return chainlinkNodesEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const entries = await getCollection("chainlink-nodes") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = entries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "chainlink-nodes" : `chainlink-nodes/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/chainlink-nodes/[...id].md.ts b/src/pages/chainlink-nodes/[...id].md.ts new file mode 100644 index 00000000000..25213b31358 --- /dev/null +++ b/src/pages/chainlink-nodes/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "chainlink-nodes" : `chainlink-nodes/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/cre-templates.md.ts b/src/pages/cre-templates.md.ts new file mode 100644 index 00000000000..e7aef84db5c --- /dev/null +++ b/src/pages/cre-templates.md.ts @@ -0,0 +1,17 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ request }) => { + const markdown = await buildMarkdownDocumentFromPath("cre/templates", request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/cre-templates/[...id].astro b/src/pages/cre-templates/[...id].astro index a680bc0f7f7..d629a49a440 100644 --- a/src/pages/cre-templates/[...id].astro +++ b/src/pages/cre-templates/[...id].astro @@ -1,27 +1,52 @@ --- +export const prerender = false + import { getCollection, render } from "astro:content" import CRETemplateLayout from "~/layouts/CRETemplateLayout.astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" -export async function getStaticPaths() { - const creTemplatesEntries = await getCollection("cre-templates") +const requestedId = Astro.params.id?.replace(/^\/+|\/+$/g, "") || "index" - return creTemplatesEntries.map((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") +const creTemplatesEntries = await getCollection("cre-templates") - return { - params: { id: routeId }, - props: { entry }, - } - }) -} +const entry = creTemplatesEntries.find((candidate) => { + const routeId = candidate.id.replace(/\.(md|mdx)$/, "") + return routeId === requestedId +}) -interface Props { - entry: Awaited>>[number] +if (!entry) { + return new Response("Page not found.", { status: 404 }) } -const { entry } = Astro.props +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "cre-templates" : `cre-templates/${requestedId}` + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} const { Content, headings } = await render(entry) + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- diff --git a/src/pages/cre-templates/[...id].md.ts b/src/pages/cre-templates/[...id].md.ts new file mode 100644 index 00000000000..f981fcd623f --- /dev/null +++ b/src/pages/cre-templates/[...id].md.ts @@ -0,0 +1,20 @@ +import type { APIRoute } from "astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" + +export const prerender = false + +export const GET: APIRoute = async ({ params, request }) => { + const requestedId = params.id?.replace(/^\/+|\/+$/g, "") || "index" + const canonicalPath = requestedId === "index" ? "cre-templates" : `cre-templates/${requestedId}` + + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: markdownHeaders, + }) +} diff --git a/src/pages/cre-templates/index.astro b/src/pages/cre-templates/index.astro index 3f3f47dbf29..047d5700750 100644 --- a/src/pages/cre-templates/index.astro +++ b/src/pages/cre-templates/index.astro @@ -1,7 +1,10 @@ --- +export const prerender = false + import { getCollection } from "astro:content" import BaseLayout from "~/layouts/BaseLayout.astro" import TemplateCard from "~/components/CRETemplate/TemplateCard.astro" +import { buildMarkdownDocumentFromPath, markdownHeaders } from "@lib/markdown/buildMarkdownResponse.js" const templates = await getCollection("cre-templates") const featuredTemplate = templates.find((t) => t.data.featured) @@ -9,6 +12,35 @@ const featuredTemplate = templates.find((t) => t.data.featured) const pageTitle = "CRE Templates Hub | Chainlink Documentation" const pageDescription = "Explore workflow templates for the Chainlink Runtime Environment. Clone and customize these templates to build your own CRE workflows." + +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = "cre-templates" +const markdownSourcePath = "cre/templates" + +if (wantsMarkdown) { + const markdown = await buildMarkdownDocumentFromPath(markdownSourcePath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) +} + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- () +const standalonePages: typeof allEntries = [] -export async function getStaticPaths() { - const allEntries = await getCollection("cre") - const pagesByPageId = new Map() - const standalonePages: any[] = [] - - // Group entries by pageId or treat as standalone - for (const entry of allEntries) { - if (entry.data.pageId && entry.data.sdkLang) { - if (!pagesByPageId.has(entry.data.pageId)) { - pagesByPageId.set(entry.data.pageId, []) - } - pagesByPageId.get(entry.data.pageId)!.push(entry) - } else { - standalonePages.push(entry) +for (const entry of allEntries) { + if (entry.data.pageId && entry.data.sdkLang) { + if (!pagesByPageId.has(entry.data.pageId)) { + pagesByPageId.set(entry.data.pageId, []) } + pagesByPageId.get(entry.data.pageId)!.push(entry) + } else { + standalonePages.push(entry) } +} - const paths: any[] = [] - - // Create routes for grouped pages (with language-specific files) - for (const entries of pagesByPageId.values()) { - const goEntry = entries.find((e) => e.data.sdkLang === "go") - const tsEntry = entries.find((e) => e.data.sdkLang === "ts") - - if (goEntry && tsEntry) { - // Create canonical URL (without language suffix) that redirects - const canonicalPath = goEntry.id.replace(/-go(\.mdx?)?$/, "") - paths.push({ - params: { id: canonicalPath }, - props: { - entry: null, - goEntry, - tsEntry, - isCanonical: true, - }, - }) - - // Create language-specific URLs: /cre/page-go and /cre/page-ts - paths.push({ - params: { id: goEntry.id }, - props: { - entry: goEntry, - goEntry, - tsEntry, - isCanonical: false, - }, - }) - - paths.push({ - params: { id: tsEntry.id }, - props: { - entry: tsEntry, - goEntry, - tsEntry, - isCanonical: false, - }, - }) +type RouteMatch = + | { + kind: "canonical" + currentEntry: (typeof allEntries)[number] + goEntry: (typeof allEntries)[number] + tsEntry: (typeof allEntries)[number] + } + | { + kind: "language" + currentEntry: (typeof allEntries)[number] + goEntry: (typeof allEntries)[number] | null + tsEntry: (typeof allEntries)[number] | null } + | { + kind: "standalone" + currentEntry: (typeof allEntries)[number] + goEntry: null + tsEntry: null + } + | { + kind: "not-found" + } + +function stripExt(id: string) { + return id.replace(/\.(md|mdx)$/, "") +} + +let match: RouteMatch = { kind: "not-found" } + +// First check grouped language pages +for (const entries of pagesByPageId.values()) { + const goEntry = entries.find((e) => e.data.sdkLang === "go") + const tsEntry = entries.find((e) => e.data.sdkLang === "ts") + + if (!goEntry || !tsEntry) continue + + const canonicalId = stripExt(goEntry.id).replace(/-go$/, "") + const goId = stripExt(goEntry.id) + const tsId = stripExt(tsEntry.id) + + if (requestedId === canonicalId) { + match = { + kind: "canonical", + currentEntry: goEntry, + goEntry, + tsEntry, + } + break } - // Standalone pages (single file, no language variants) - standalonePages.forEach((entry) => { - const routeId = entry.id.replace(/\.(md|mdx)$/, "") - paths.push({ - params: { id: routeId }, - props: { - entry, - isCanonical: false, - goEntry: null, - tsEntry: null, - }, - }) - }) + if (requestedId === goId) { + match = { + kind: "language", + currentEntry: goEntry, + goEntry, + tsEntry, + } + break + } + + if (requestedId === tsId) { + match = { + kind: "language", + currentEntry: tsEntry, + goEntry, + tsEntry, + } + break + } +} + +// Then standalone pages +if (match.kind === "not-found") { + const standaloneEntry = standalonePages.find((entry) => stripExt(entry.id) === requestedId) - return paths + if (standaloneEntry) { + match = { + kind: "standalone", + currentEntry: standaloneEntry, + goEntry: null, + tsEntry: null, + } + } } -interface Props { - entry: Awaited>>[number] | null - goEntry: Awaited>>[number] | null - tsEntry: Awaited>>[number] | null - isCanonical: boolean +if (match.kind === "not-found") { + return new Response("Page not found.", { status: 404 }) } -const { entry, goEntry, tsEntry, isCanonical } = Astro.props - -// Determine what to render -let Content, headings, currentEntry - -if (isCanonical) { - // Canonical URL - will redirect client-side based on preference - currentEntry = goEntry || tsEntry! - const renderResult = await render(currentEntry) - Content = () => null // Don't render content, just redirect - headings = renderResult.headings -} else { - // Language-specific URL - render the content - currentEntry = entry! - const renderResult = await render(currentEntry) - Content = renderResult.Content - headings = renderResult.headings +const accept = Astro.request.headers.get("accept") || "" +const wantsMarkdown = accept + .toLowerCase() + .split(",") + .map((part) => part.split(";")[0]?.trim()) + .includes("text/markdown") + +const canonicalPath = requestedId === "index" ? "cre" : `cre/${requestedId}` + +const renderResult = await render(match.currentEntry) +const headings = renderResult.headings +const Content = match.kind === "canonical" ? () => null : renderResult.Content + +// Only language-specific and standalone pages should negotiate to markdown. +// Canonical CRE pages are redirect shells. +if (wantsMarkdown && match.kind !== "canonical") { + const markdown = await buildMarkdownDocumentFromPath(canonicalPath, Astro.request) + + if (!markdown) { + return new Response("Page not found.", { status: 404 }) + } + + return new Response(markdown, { + status: 200, + headers: { + ...markdownHeaders, + Vary: "Accept", + }, + }) } + +Astro.response.headers.append("Link", `; rel="alternate"; type="text/markdown"`) +Astro.response.headers.set("Vary", "Accept") --- - - {/* noindex for redirect pages */} - {isCanonical && } + + {match.kind === "canonical" && } { - isCanonical && ( + match.kind === "canonical" && (