diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ad9896723..4f2dd0426 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -30,6 +30,19 @@ jobs: - name: Validate RPC specs run: pnpm run generate:rpc + docs-yml: + name: docs.yml Schema + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + - name: Validate docs.yml against schema + run: pnpm run validate:docs-yml + lint: name: Lint Files runs-on: ubuntu-latest diff --git a/content/docs-yml.schema.json b/content/docs-yml.schema.json new file mode 100644 index 000000000..86bfd3e9e --- /dev/null +++ b/content/docs-yml.schema.json @@ -0,0 +1,179 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Alchemy docs.yml", + "description": "Navigation configuration for the Alchemy docs site. Allows only the options supported by the content indexer (src/content-indexer/types/docsYaml.ts), including Alchemy-specific extensions (skip-slug, flattened).", + "type": "object", + "additionalProperties": false, + "required": ["navigation"], + "properties": { + "tabs": { + "description": "Top-level tabs shown in the docs header. Keys are tab identifiers referenced by navigation[].tab.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/tabConfig" + } + }, + "navigation": { + "description": "Sidebar navigation layout for each tab.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["tab"], + "properties": { + "tab": { + "description": "Tab identifier; must match a key under tabs.", + "type": "string" + }, + "layout": { + "description": "Navigation items for this tab. Omit for tabs whose content is generated elsewhere (e.g. the changelog tab).", + "type": "array", + "items": { + "$ref": "#/definitions/navigationItem" + } + } + } + } + } + }, + "definitions": { + "tabConfig": { + "type": "object", + "additionalProperties": false, + "required": ["display-name"], + "properties": { + "display-name": { + "description": "Human-readable tab label shown in the header.", + "type": "string" + }, + "slug": { + "description": "URL segment for the tab. Defaults to the kebab-cased tab key.", + "type": "string" + }, + "skip-slug": { + "description": "Alchemy extension: omit this tab's slug from descendant URL paths.", + "type": "boolean" + } + } + }, + "navigationItem": { + "oneOf": [ + { "$ref": "#/definitions/pageItem" }, + { "$ref": "#/definitions/sectionItem" }, + { "$ref": "#/definitions/linkItem" }, + { "$ref": "#/definitions/apiItem" } + ] + }, + "mdxPath": { + "type": "string", + "pattern": "\\.mdx$" + }, + "pageItem": { + "description": "A single docs page backed by an MDX file.", + "type": "object", + "additionalProperties": false, + "required": ["page", "path"], + "properties": { + "page": { + "description": "Page title shown in the sidebar.", + "type": "string" + }, + "path": { + "description": "Path to the page's .mdx file, relative to the content directory.", + "$ref": "#/definitions/mdxPath" + }, + "slug": { + "description": "URL segment for the page. Defaults to the kebab-cased page title. May contain slashes to override the full path.", + "type": "string" + }, + "hidden": { + "description": "Hide this page from the sidebar and search indexing.", + "type": "boolean" + } + } + }, + "sectionItem": { + "description": "A collapsible group of navigation items, optionally with an overview page.", + "type": "object", + "additionalProperties": false, + "required": ["section", "contents"], + "properties": { + "section": { + "description": "Section title shown in the sidebar.", + "type": "string" + }, + "contents": { + "description": "Child navigation items.", + "type": "array", + "items": { + "$ref": "#/definitions/navigationItem" + } + }, + "path": { + "description": "Optional overview page for the section: path to an .mdx file, relative to the content directory.", + "$ref": "#/definitions/mdxPath" + }, + "slug": { + "description": "URL segment for the section. Defaults to the kebab-cased section title.", + "type": "string" + }, + "skip-slug": { + "description": "Alchemy extension: omit this section's slug from descendant URL paths.", + "type": "boolean" + }, + "hidden": { + "description": "Hide this section and its contents from the sidebar and search indexing.", + "type": "boolean" + } + } + }, + "linkItem": { + "description": "A sidebar entry linking to an internal path or external URL.", + "type": "object", + "additionalProperties": false, + "required": ["link", "href"], + "properties": { + "link": { + "description": "Link text shown in the sidebar.", + "type": "string" + }, + "href": { + "description": "Destination: an internal path (e.g. /docs/...) or an external URL.", + "type": "string" + } + } + }, + "apiItem": { + "description": "An auto-generated API reference section backed by an uploaded API spec.", + "type": "object", + "additionalProperties": false, + "required": ["api", "api-name"], + "properties": { + "api": { + "description": "Title for the API reference section.", + "type": "string" + }, + "api-name": { + "description": "Identifier of the API spec to render (must match an uploaded spec name).", + "type": "string" + }, + "slug": { + "description": "URL segment for the API section. Defaults to the kebab-cased title.", + "type": "string" + }, + "skip-slug": { + "description": "Alchemy extension: omit this section's slug from endpoint URL paths.", + "type": "boolean" + }, + "hidden": { + "description": "Hide the endpoints from the sidebar and search indexing.", + "type": "boolean" + }, + "flattened": { + "description": "Alchemy extension: render endpoints as a flat list instead of grouped by spec hierarchy.", + "type": "boolean" + } + } + } + } +} diff --git a/content/docs.yml b/content/docs.yml index 6d55de36a..2cb487427 100644 --- a/content/docs.yml +++ b/content/docs.yml @@ -1,7 +1,4 @@ -# yaml-language-server: $schema=https://schema.buildwithfern.dev/docs-yml.json - -instances: - - url: https://alchemy.com/docs +# yaml-language-server: $schema=./docs-yml.schema.json tabs: get-started: @@ -24,7 +21,6 @@ tabs: slug: rollups changelog: display-name: Changelog - changelog: ./changelog slug: changelog navigation: @@ -879,7 +875,6 @@ navigation: - page: Using API path: wallets/pages/smart-wallets/quickstart/api.mdx - section: Send transactions - collapsed: true contents: - page: Single transactions path: wallets/pages/transactions/send-transactions/index.mdx @@ -889,19 +884,16 @@ navigation: path: wallets/pages/transactions/send-parallel-transactions/index.mdx - section: Debug transactions path: wallets/pages/transactions/debug-transactions/index.mdx - collapsed: true contents: - page: Debug with Tenderly path: wallets/pages/transactions/debug-transactions/debug-with-tenderly.mdx - section: EIP-7702 path: wallets/pages/transactions/using-eip-7702/index.mdx - collapsed: true contents: - page: Undelegate 7702 account path: wallets/pages/transactions/undelegate-account/index.mdx - section: Sponsor gas path: wallets/pages/transactions/sponsor-gas/overview.mdx - collapsed: true contents: - page: Full sponsorship path: wallets/pages/transactions/sponsor-gas/index.mdx @@ -912,14 +904,12 @@ navigation: - page: Solana sponsorship path: wallets/pages/transactions/sponsor-gas/solana/index.mdx - section: Swap tokens - collapsed: true contents: - page: Same-chain swaps path: wallets/pages/transactions/swap-tokens/index.mdx - page: Cross-chain swaps path: wallets/pages/transactions/cross-chain-swap-tokens/index.mdx - section: Grant session keys - collapsed: true contents: - page: Overview path: wallets/pages/smart-wallets/session-keys/index.mdx @@ -932,7 +922,6 @@ navigation: - page: Retry transactions path: wallets/pages/transactions/retry-transactions/index.mdx - section: Sign - collapsed: true contents: - page: Sign messages path: wallets/pages/transactions/signing/sign-messages/index.mdx @@ -947,7 +936,6 @@ navigation: path: wallets/pages/authentication/overview.mdx - section: Privy path: wallets/pages/third-party/signers/privy.mdx - collapsed: true contents: - page: Signer migration overview path: wallets/wallet-integrations/privy/signer-migration-overview.mdx @@ -966,10 +954,8 @@ navigation: - page: Other signers path: wallets/pages/third-party/signers/custom-integration.mdx - section: Account Kit (v4) - collapsed: true contents: - section: Login methods - collapsed: true contents: - page: Email OTP path: wallets/pages/authentication/login-methods/email-otp.mdx @@ -992,7 +978,6 @@ navigation: - page: Adding and removing login methods path: wallets/pages/react/login-methods/adding-and-removing-login-methods.mdx - section: UI components - collapsed: true contents: - page: Using UI components path: wallets/pages/react/ui-components.mdx @@ -1001,21 +986,18 @@ navigation: - page: Tailwind setup path: wallets/pages/react/customization/tailwind-setup.mdx - section: Whitelabel - collapsed: true contents: - page: React hooks path: wallets/pages/react/react-hooks.mdx - page: Other JS initialization path: wallets/pages/signer/quickstart.mdx - section: Connectors - collapsed: true contents: - page: Connect external wallets path: wallets/pages/react/login-methods/eoa-login.mdx - page: Customize path: wallets/pages/react/connectors/customization.mdx - section: MFA - collapsed: true contents: - page: Set up MFA path: wallets/pages/react/mfa/setup-mfa.mdx diff --git a/package.json b/package.json index d719e99a0..6e868ddf2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "clean": "rm -rf content/api-specs", "validate:rest": "./scripts/generate-open-api.sh --validate-only", "validate:rpc": "tsx ./scripts/validate-rpc.ts", + "validate:docs-yml": "tsx ./scripts/validate-docs-yml.ts", "validate": "pnpm run validate:rest & pnpm run validate:rpc", "test:run": "vitest run", "test:coverage": "vitest run --coverage", @@ -64,6 +65,7 @@ "@types/remove-markdown": "^0.3.4", "@typescript-eslint/parser": "^8.31.0", "@vitest/coverage-v8": "^4.0.16", + "ajv": "^8.20.0", "eslint": "^9.25.1", "eslint-config-prettier": "^10.1.2", "eslint-plugin-mdx": "^3.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3151f2de..ecf1280c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,7 +47,7 @@ importers: version: 1.14.9 '@redocly/cli': specifier: ^1.34.2 - version: 1.34.11(ajv@6.14.0) + version: 1.34.11(ajv@8.20.0) '@trivago/prettier-plugin-sort-imports': specifier: ^5.2.2 version: 5.2.2(prettier@3.4.2) @@ -75,6 +75,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.0.16 version: 4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.9.0))) + ajv: + specifier: ^8.20.0 + version: 8.20.0 eslint: specifier: ^9.25.1 version: 9.39.4(jiti@2.6.1) @@ -1084,6 +1087,9 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + algoliasearch@5.49.2: resolution: {integrity: sha512-1K0wtDaRONwfhL4h8bbJ9qTjmY6rhGgRvvagXkMBsAOMNr+3Q2SffHECh9DIuNVrMA1JwA0zCwhyepgBZVakng==} engines: {node: '>= 14.0.0'} @@ -1558,6 +1564,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-xml-builder@1.0.0: resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==} @@ -3898,7 +3907,7 @@ snapshots: require-from-string: 2.0.2 uri-js-replace: 1.0.1 - '@redocly/cli@1.34.11(ajv@6.14.0)': + '@redocly/cli@1.34.11(ajv@8.20.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-http': 0.53.0(@opentelemetry/api@1.9.0) @@ -3907,7 +3916,7 @@ snapshots: '@opentelemetry/semantic-conventions': 1.27.0 '@redocly/config': 0.22.0 '@redocly/openapi-core': 1.34.11 - '@redocly/respect-core': 1.34.11(ajv@6.14.0) + '@redocly/respect-core': 1.34.11(ajv@8.20.0) abort-controller: 3.0.0 chokidar: 3.5.3 colorette: 1.4.0 @@ -3950,12 +3959,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@redocly/respect-core@1.34.11(ajv@6.14.0)': + '@redocly/respect-core@1.34.11(ajv@8.20.0)': dependencies: '@faker-js/faker': 7.6.0 '@redocly/ajv': 8.11.2 '@redocly/openapi-core': 1.34.11 - better-ajv-errors: 1.2.0(ajv@6.14.0) + better-ajv-errors: 1.2.0(ajv@8.20.0) colorette: 2.0.20 concat-stream: 2.0.0 cookie: 0.7.2 @@ -4305,6 +4314,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.49.2: dependencies: '@algolia/abtesting': 1.15.2 @@ -4367,11 +4383,11 @@ snapshots: balanced-match@4.0.4: {} - better-ajv-errors@1.2.0(ajv@6.14.0): + better-ajv-errors@1.2.0(ajv@8.20.0): dependencies: '@babel/code-frame': 7.29.0 '@humanwhocodes/momoa': 2.0.4 - ajv: 6.14.0 + ajv: 8.20.0 chalk: 4.1.2 jsonpointer: 5.0.1 leven: 3.1.0 @@ -4817,6 +4833,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-uri@3.1.2: {} + fast-xml-builder@1.0.0: {} fast-xml-parser@5.4.1: diff --git a/scripts/validate-docs-yml.ts b/scripts/validate-docs-yml.ts new file mode 100644 index 000000000..7a5101598 --- /dev/null +++ b/scripts/validate-docs-yml.ts @@ -0,0 +1,27 @@ +import { Ajv } from "ajv"; +import { readFileSync } from "fs"; +import yaml from "js-yaml"; + +const DOCS_YML_PATH = "content/docs.yml"; +const SCHEMA_PATH = "content/docs-yml.schema.json"; + +const validateDocsYml = () => { + const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8")); + const docsYml = yaml.load(readFileSync(DOCS_YML_PATH, "utf8")); + + const ajv = new Ajv({ allErrors: true }); + const validate = ajv.compile(schema); + + if (!validate(docsYml)) { + const messages = new Set( + (validate.errors ?? []).map( + (error) => `❌ ${DOCS_YML_PATH}${error.instancePath}: ${error.message}`, + ), + ); + throw new Error([...messages].join("\n")); + } + + console.info(`✅ Successfully validated ${DOCS_YML_PATH}`); +}; + +validateDocsYml(); diff --git a/src/content-indexer/core/path-builder.ts b/src/content-indexer/core/path-builder.ts index a9fa30de6..7e8d01cea 100644 --- a/src/content-indexer/core/path-builder.ts +++ b/src/content-indexer/core/path-builder.ts @@ -1,8 +1,7 @@ /** - * PathBuilder mimics Fern's slug generation logic to build full URL paths. + * PathBuilder mimics the prior docs provider's slug generation logic to build full URL paths. * Maintains an array of path segments and provides methods to build paths hierarchically. - * @note Fern incorrectly refers to full paths as "slugs" in their terminology - * @see https://buildwithfern.com/learn/docs/seo/configuring-slugs + * @note The prior docs provider incorrectly referred to full paths as "slugs" in its terminology */ export class PathBuilder { private segments: string[]; diff --git a/src/content-indexer/types/docsYaml.ts b/src/content-indexer/types/docsYaml.ts index 77d70b09e..82e01f675 100644 --- a/src/content-indexer/types/docsYaml.ts +++ b/src/content-indexer/types/docsYaml.ts @@ -5,7 +5,6 @@ export interface PageConfig { path: string; slug?: string; hidden?: boolean; - noindex?: boolean; } export interface SectionConfig { @@ -29,7 +28,6 @@ export interface ApiConfig { "skip-slug"?: boolean; hidden?: boolean; flattened?: boolean; - paginated?: boolean; } export interface ChangelogConfig { @@ -54,7 +52,7 @@ export interface DocsYml { tabs?: Record; navigation: Array<{ tab: string; - layout: NavigationItem[]; + layout?: NavigationItem[]; }>; } diff --git a/src/content-indexer/types/page.ts b/src/content-indexer/types/page.ts index b794cbdd9..e46c22fbc 100644 --- a/src/content-indexer/types/page.ts +++ b/src/content-indexer/types/page.ts @@ -1,6 +1,5 @@ /** * Frontmatter for a documentation page - * @see https://buildwithfern.com/learn/docs/configuration/page-level-settings */ export interface DocPageFrontmatter { title?: string;