From 0ad62d811c3e9fd7290689f1f94ded5ef6203f52 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Wed, 24 Jun 2026 21:44:01 -0400 Subject: [PATCH 1/3] feat: add related links and rework the docs corpus generator Normalize corpus slugs to a consistent all-dashes scheme and reshape the catalog: short-summary descriptions, a kind facet (guide/reference/endpoint), an endpoint facet carrying operationId, path, and method, and path-derived topics so no doc is left uncategorized. Drop nav-shell pages and strip the generated APIPage bodies while keeping the endpoint catalog entries. Add curated related links. Each page declares related URLs in frontmatter; the generator resolves internal links to doc ids, emits per-doc outbound edges, and renders a Related section in the corpus. The docs site renders the same links as Related cards at the bottom of every page. --- content/docs/changelog/index.mdx | 4 + .../agentic-guidance/capability-map.mdx | 5 + .../agentic-guidance/entity-shapes.mdx | 5 + .../agentic-guidance/integration-guide.mdx | 5 + .../documentation/events/event-payload.mdx | 4 + .../docs/documentation/events/event-types.mdx | 4 + .../docs/documentation/events/get-event.mdx | 4 + content/docs/documentation/events/index.mdx | 4 + .../documentation/events/search-events.mdx | 4 + content/docs/documentation/faq.mdx | 5 + .../docs/documentation/getting-started.mdx | 5 + .../documentation/getting-trial-api-key.mdx | 4 + .../making-your-first-request.mdx | 4 + content/docs/documentation/mcp/index.mdx | 5 + .../person-search/confidence-scores.mdx | 4 + .../person-search/filter-by-age-range.mdx | 4 + .../person-search/fuzzy-matching.mdx | 4 + .../documentation/person-search/index.mdx | 5 + .../person-search/pagination.mdx | 4 + .../person-search/partial-name-search.mdx | 4 + .../person-search/reverse-phone-lookup.mdx | 5 + .../person-search/search-by-address.mdx | 5 + .../person-search/search-by-radius.mdx | 4 + .../docs/documentation/property-search.mdx | 5 + content/docs/documentation/purchasing-api.mdx | 4 + content/docs/documentation/regions.mdx | 5 + .../documentation/webhooks/create-webhook.mdx | 5 + .../documentation/webhooks/delete-webhook.mdx | 4 + content/docs/documentation/webhooks/index.mdx | 4 + .../documentation/webhooks/list-webhooks.mdx | 4 + .../documentation/webhooks/quickstart.mdx | 4 + .../documentation/webhooks/test-webhook.mdx | 4 + .../documentation/webhooks/update-webhook.mdx | 4 + .../account/get_account_usage_data.mdx | 4 + content/docs/references/authentication.mdx | 4 + .../billing-prior-to-2026-06-01.mdx | 3 + content/docs/references/billing.mdx | 5 + content/docs/references/events/get_event.mdx | 4 + .../references/events/search_deed_events.mdx | 5 + .../docs/references/events/search_events.mdx | 4 + content/docs/references/migration/index.mdx | 5 + .../references/migration/person-v1-to-v2.mdx | 5 + .../migration/property-v1-to-v2.mdx | 5 + .../person-v2/get_person_by_id_v2.mdx | 5 + ...rch_person_by_name_phone_or_address_v2.mdx | 6 + .../property-v2/get_property_by_id_v2.mdx | 5 + .../property-v2/search_property_v2.mdx | 6 + content/docs/references/rate-limits.mdx | 5 + .../docs/references/regions/list_counties.mdx | 5 + .../docs/references/regions/list_states.mdx | 4 + .../support-and-incident-response.mdx | 4 + .../versioning/breaking-changes-policy.mdx | 4 + .../versioning/change-notifications.mdx | 4 + .../references/webhooks/create_webhook.mdx | 5 + .../references/webhooks/delete_webhook.mdx | 4 + .../docs/references/webhooks/get_webhook.mdx | 4 + .../references/webhooks/list_webhooks.mdx | 4 + .../docs/references/webhooks/test_webhook.mdx | 4 + .../references/webhooks/update_webhook.mdx | 4 + scripts/generators/corpus/corpus.core.test.ts | 466 ++++++++++++++++-- scripts/generators/corpus/corpus.core.ts | 279 ++++++++++- scripts/generators/corpus/corpus.generator.ts | 35 +- scripts/generators/corpus/corpus.types.ts | 17 +- .../generators/openapi/openapi.facets.test.ts | 47 +- scripts/generators/openapi/openapi.facets.ts | 105 +++- src/app/[[...slug]]/page.tsx | 17 + src/components/related-links.tsx | 33 ++ src/lib/facets.ts | 1 + 68 files changed, 1207 insertions(+), 54 deletions(-) create mode 100644 src/components/related-links.tsx diff --git a/content/docs/changelog/index.mdx b/content/docs/changelog/index.mdx index aabf5c5..48d926d 100644 --- a/content/docs/changelog/index.mdx +++ b/content/docs/changelog/index.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/versioning/change-notifications + - /references/versioning/breaking-changes-policy + - /references/migration title: Changelog description: Notable changes to the Whitepages API, newest first. --- diff --git a/content/docs/documentation/agentic-guidance/capability-map.mdx b/content/docs/documentation/agentic-guidance/capability-map.mdx index 9e11648..0be3921 100644 --- a/content/docs/documentation/agentic-guidance/capability-map.mdx +++ b/content/docs/documentation/agentic-guidance/capability-map.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/agentic-guidance/integration-guide + - /documentation/agentic-guidance/entity-shapes + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /references/property-v2/search_property_v2 title: Capability Map description: Map a user's intent to the exact endpoint and parameters that answer it. icon: Compass diff --git a/content/docs/documentation/agentic-guidance/entity-shapes.mdx b/content/docs/documentation/agentic-guidance/entity-shapes.mdx index 6eda430..d10dee8 100644 --- a/content/docs/documentation/agentic-guidance/entity-shapes.mdx +++ b/content/docs/documentation/agentic-guidance/entity-shapes.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/agentic-guidance/capability-map + - /documentation/agentic-guidance/integration-guide + - /references/person-v2/get_person_by_id_v2 + - /references/property-v2/get_property_by_id_v2 title: Entity Shapes by Context description: The same entity comes back in different shapes depending on the endpoint. Do not assume one shape. icon: Boxes diff --git a/content/docs/documentation/agentic-guidance/integration-guide.mdx b/content/docs/documentation/agentic-guidance/integration-guide.mdx index fcfcc67..b6cc0c6 100644 --- a/content/docs/documentation/agentic-guidance/integration-guide.mdx +++ b/content/docs/documentation/agentic-guidance/integration-guide.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/agentic-guidance/capability-map + - /documentation/agentic-guidance/entity-shapes + - /references/versioning/breaking-changes-policy + - /documentation/mcp title: Integration Guide description: The workflow to follow when integrating the Whitepages API, by hand or with an agent. icon: Workflow diff --git a/content/docs/documentation/events/event-payload.mdx b/content/docs/documentation/events/event-payload.mdx index 13a4665..70f35ad 100644 --- a/content/docs/documentation/events/event-payload.mdx +++ b/content/docs/documentation/events/event-payload.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/events/event-types + - /documentation/events/get-event + - /references/events/get_event title: Event Payload description: Explore the full structure of an event object --- diff --git a/content/docs/documentation/events/event-types.mdx b/content/docs/documentation/events/event-types.mdx index 34dade4..c941743 100644 --- a/content/docs/documentation/events/event-types.mdx +++ b/content/docs/documentation/events/event-types.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/events + - /documentation/webhooks/create-webhook + - /documentation/events/event-payload title: Event Types description: Understand the different types of property deed events --- diff --git a/content/docs/documentation/events/get-event.mdx b/content/docs/documentation/events/get-event.mdx index ac8997f..75d994a 100644 --- a/content/docs/documentation/events/get-event.mdx +++ b/content/docs/documentation/events/get-event.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/events/get_event + - /documentation/events/event-payload + - /documentation/events title: Get an Event description: Retrieve a specific property deed event by its unique identifier --- diff --git a/content/docs/documentation/events/index.mdx b/content/docs/documentation/events/index.mdx index 2e34e58..e1dc85e 100644 --- a/content/docs/documentation/events/index.mdx +++ b/content/docs/documentation/events/index.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks + - /references/events/search_deed_events + - /documentation/regions title: Events description: Retrieve and understand property deed events from the Whitepages API --- diff --git a/content/docs/documentation/events/search-events.mdx b/content/docs/documentation/events/search-events.mdx index 691064a..e89ea0a 100644 --- a/content/docs/documentation/events/search-events.mdx +++ b/content/docs/documentation/events/search-events.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/events/search_deed_events + - /documentation/events/event-payload + - /documentation/regions title: Search Deed Events description: Query historical property deed events by region, date range, and property identifier --- diff --git a/content/docs/documentation/faq.mdx b/content/docs/documentation/faq.mdx index e06c493..8c25d6b 100644 --- a/content/docs/documentation/faq.mdx +++ b/content/docs/documentation/faq.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/agentic-guidance/capability-map + - /references/rate-limits + - /documentation/purchasing-api + - /references/versioning/breaking-changes-policy title: FAQ description: Answers to the questions integrators hit most often. icon: CircleQuestionMark diff --git a/content/docs/documentation/getting-started.mdx b/content/docs/documentation/getting-started.mdx index 72ba275..41d03b0 100644 --- a/content/docs/documentation/getting-started.mdx +++ b/content/docs/documentation/getting-started.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/making-your-first-request + - /documentation/getting-trial-api-key + - /documentation/person-search + - /documentation/property-search title: Getting Started description: Get started with the Whitepages API icon: Rocket diff --git a/content/docs/documentation/getting-trial-api-key.mdx b/content/docs/documentation/getting-trial-api-key.mdx index 5ea0fe4..e597d83 100644 --- a/content/docs/documentation/getting-trial-api-key.mdx +++ b/content/docs/documentation/getting-trial-api-key.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/making-your-first-request + - /documentation/purchasing-api + - /references/authentication title: Getting a Trial API Key description: Step-by-step guide to signing up for a free Whitepages API trial icon: Key diff --git a/content/docs/documentation/making-your-first-request.mdx b/content/docs/documentation/making-your-first-request.mdx index 6d78ab2..3e4d2ce 100644 --- a/content/docs/documentation/making-your-first-request.mdx +++ b/content/docs/documentation/making-your-first-request.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/getting-trial-api-key + - /documentation/person-search + - /documentation/agentic-guidance/capability-map title: Making Your First Request description: A beginner-friendly guide to making your first Whitepages API request icon: Play diff --git a/content/docs/documentation/mcp/index.mdx b/content/docs/documentation/mcp/index.mdx index 4aa3999..057fe01 100644 --- a/content/docs/documentation/mcp/index.mdx +++ b/content/docs/documentation/mcp/index.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/getting-trial-api-key + - /documentation/agentic-guidance/capability-map + - /documentation/agentic-guidance/integration-guide + - /references/rate-limits title: MCP Server description: Connect to Whitepages API via Model Context Protocol for AI assistants icon: Plug diff --git a/content/docs/documentation/person-search/confidence-scores.mdx b/content/docs/documentation/person-search/confidence-scores.mdx index 5ec1cd5..c51aa40 100644 --- a/content/docs/documentation/person-search/confidence-scores.mdx +++ b/content/docs/documentation/person-search/confidence-scores.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search/fuzzy-matching + - /documentation/person-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 title: Confidence Scores description: How to interpret person, phone, and email confidence scores lifecycle: current diff --git a/content/docs/documentation/person-search/filter-by-age-range.mdx b/content/docs/documentation/person-search/filter-by-age-range.mdx index 2cb2086..7ca6e40 100644 --- a/content/docs/documentation/person-search/filter-by-age-range.mdx +++ b/content/docs/documentation/person-search/filter-by-age-range.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search + - /documentation/person-search/partial-name-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 title: Filter by Age Range description: Narrow person search results using minimum and maximum age filters lifecycle: current diff --git a/content/docs/documentation/person-search/fuzzy-matching.mdx b/content/docs/documentation/person-search/fuzzy-matching.mdx index 2529651..5fe6882 100644 --- a/content/docs/documentation/person-search/fuzzy-matching.mdx +++ b/content/docs/documentation/person-search/fuzzy-matching.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search/confidence-scores + - /documentation/person-search/partial-name-search + - /documentation/person-search title: Fuzzy Matching description: Broaden person search results with phonetic, nickname, and misspelling matching lifecycle: current diff --git a/content/docs/documentation/person-search/index.mdx b/content/docs/documentation/person-search/index.mdx index f98e1c6..565552f 100644 --- a/content/docs/documentation/person-search/index.mdx +++ b/content/docs/documentation/person-search/index.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /documentation/agentic-guidance/capability-map + - /documentation/person-search/reverse-phone-lookup + - /documentation/person-search/search-by-address title: Person Search description: Look up individuals by name, phone, or address lifecycle: current diff --git a/content/docs/documentation/person-search/pagination.mdx b/content/docs/documentation/person-search/pagination.mdx index d61962f..4e5ceab 100644 --- a/content/docs/documentation/person-search/pagination.mdx +++ b/content/docs/documentation/person-search/pagination.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search + - /references/billing + - /references/person-v2/search_person_by_name_phone_or_address_v2 title: Pagination description: Page through person search results using the page and page_size parameters lifecycle: current diff --git a/content/docs/documentation/person-search/partial-name-search.mdx b/content/docs/documentation/person-search/partial-name-search.mdx index 8022383..6290735 100644 --- a/content/docs/documentation/person-search/partial-name-search.mdx +++ b/content/docs/documentation/person-search/partial-name-search.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search/fuzzy-matching + - /documentation/person-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 title: Search for Person by Partial Name description: Search for people using partial or incomplete name information lifecycle: current diff --git a/content/docs/documentation/person-search/reverse-phone-lookup.mdx b/content/docs/documentation/person-search/reverse-phone-lookup.mdx index b3bd392..a4e3bd5 100644 --- a/content/docs/documentation/person-search/reverse-phone-lookup.mdx +++ b/content/docs/documentation/person-search/reverse-phone-lookup.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/agentic-guidance/capability-map + - /documentation/person-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /documentation/person-search/search-by-address title: Reverse Phone Lookup description: Find the owner of a phone number lifecycle: current diff --git a/content/docs/documentation/person-search/search-by-address.mdx b/content/docs/documentation/person-search/search-by-address.mdx index 60bf0f2..c48b308 100644 --- a/content/docs/documentation/person-search/search-by-address.mdx +++ b/content/docs/documentation/person-search/search-by-address.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/person-search + - /documentation/property-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /documentation/person-search/search-by-radius title: Search by Address description: Find people by current or historical addresses lifecycle: current diff --git a/content/docs/documentation/person-search/search-by-radius.mdx b/content/docs/documentation/person-search/search-by-radius.mdx index 8bbd381..f9c8c1f 100644 --- a/content/docs/documentation/person-search/search-by-radius.mdx +++ b/content/docs/documentation/person-search/search-by-radius.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/person-search/search-by-address + - /documentation/person-search + - /references/person-v2/search_person_by_name_phone_or_address_v2 title: Search by Radius description: Find people within a specified distance of a location lifecycle: current diff --git a/content/docs/documentation/property-search.mdx b/content/docs/documentation/property-search.mdx index 870a6d7..7fe902f 100644 --- a/content/docs/documentation/property-search.mdx +++ b/content/docs/documentation/property-search.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/property-v2/search_property_v2 + - /references/property-v2/get_property_by_id_v2 + - /documentation/agentic-guidance/capability-map + - /documentation/person-search/search-by-address title: Property Search description: Get ownership and resident data for any property icon: House diff --git a/content/docs/documentation/purchasing-api.mdx b/content/docs/documentation/purchasing-api.mdx index a53cbd6..991a1e6 100644 --- a/content/docs/documentation/purchasing-api.mdx +++ b/content/docs/documentation/purchasing-api.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/getting-trial-api-key + - /references/billing + - /references/rate-limits title: Purchasing the API description: Step-by-step guide to purchasing a Whitepages Pro API subscription icon: CreditCard diff --git a/content/docs/documentation/regions.mdx b/content/docs/documentation/regions.mdx index 23a7fe5..2e79316 100644 --- a/content/docs/documentation/regions.mdx +++ b/content/docs/documentation/regions.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/regions/list_states + - /references/regions/list_counties + - /documentation/events/search-events + - /documentation/webhooks/create-webhook title: Regions description: Browse and search US states and counties available in the Whitepages API icon: Map diff --git a/content/docs/documentation/webhooks/create-webhook.mdx b/content/docs/documentation/webhooks/create-webhook.mdx index 1a14e23..c106c00 100644 --- a/content/docs/documentation/webhooks/create-webhook.mdx +++ b/content/docs/documentation/webhooks/create-webhook.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/webhooks/create_webhook + - /documentation/webhooks/test-webhook + - /documentation/regions + - /documentation/events/event-types title: Create a Webhook description: Register a new webhook subscription to receive property event notifications --- diff --git a/content/docs/documentation/webhooks/delete-webhook.mdx b/content/docs/documentation/webhooks/delete-webhook.mdx index d06ab37..e0914cb 100644 --- a/content/docs/documentation/webhooks/delete-webhook.mdx +++ b/content/docs/documentation/webhooks/delete-webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/webhooks/delete_webhook + - /documentation/webhooks/list-webhooks + - /documentation/webhooks title: Delete a Webhook description: Permanently remove a webhook subscription --- diff --git a/content/docs/documentation/webhooks/index.mdx b/content/docs/documentation/webhooks/index.mdx index a603f37..bd8f090 100644 --- a/content/docs/documentation/webhooks/index.mdx +++ b/content/docs/documentation/webhooks/index.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/events + - /references/webhooks/create_webhook + - /documentation/regions title: Webhooks description: Receive live property deed notifications delivered to your endpoint --- diff --git a/content/docs/documentation/webhooks/list-webhooks.mdx b/content/docs/documentation/webhooks/list-webhooks.mdx index c42eb0d..f9872cc 100644 --- a/content/docs/documentation/webhooks/list-webhooks.mdx +++ b/content/docs/documentation/webhooks/list-webhooks.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/webhooks/list_webhooks + - /references/webhooks/get_webhook + - /documentation/webhooks title: List Webhooks description: List and filter your webhook subscriptions --- diff --git a/content/docs/documentation/webhooks/quickstart.mdx b/content/docs/documentation/webhooks/quickstart.mdx index 309e216..ecfa119 100644 --- a/content/docs/documentation/webhooks/quickstart.mdx +++ b/content/docs/documentation/webhooks/quickstart.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/create-webhook + - /documentation/events/event-payload + - /documentation/regions title: Webhook Quickstart description: Walk through the full webhook lifecycle — create, test, and retrieve an event — with real API calls full: true diff --git a/content/docs/documentation/webhooks/test-webhook.mdx b/content/docs/documentation/webhooks/test-webhook.mdx index 8d72f30..0c327f4 100644 --- a/content/docs/documentation/webhooks/test-webhook.mdx +++ b/content/docs/documentation/webhooks/test-webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/webhooks/test_webhook + - /documentation/webhooks/create-webhook + - /documentation/events/get-event title: Test a Webhook description: Send a mock event payload to verify your webhook endpoint --- diff --git a/content/docs/documentation/webhooks/update-webhook.mdx b/content/docs/documentation/webhooks/update-webhook.mdx index d568f78..e6da523 100644 --- a/content/docs/documentation/webhooks/update-webhook.mdx +++ b/content/docs/documentation/webhooks/update-webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/webhooks/update_webhook + - /documentation/webhooks/list-webhooks + - /documentation/webhooks title: Update a Webhook description: Modify an existing webhook subscription --- diff --git a/content/docs/references/account/get_account_usage_data.mdx b/content/docs/references/account/get_account_usage_data.mdx index 75f0603..c2cd784 100644 --- a/content/docs/references/account/get_account_usage_data.mdx +++ b/content/docs/references/account/get_account_usage_data.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/billing + - /references/rate-limits + - /references/authentication lifecycle: current topic: account-billing keywords: diff --git a/content/docs/references/authentication.mdx b/content/docs/references/authentication.mdx index af99d10..ed49749 100644 --- a/content/docs/references/authentication.mdx +++ b/content/docs/references/authentication.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/getting-trial-api-key + - /references/rate-limits + - /references/billing title: Authentication description: Learn how to authenticate your requests to the Whitepages API. icon: KeyRound diff --git a/content/docs/references/billing-prior-to-2026-06-01.mdx b/content/docs/references/billing-prior-to-2026-06-01.mdx index b0c7c11..dc69e0f 100644 --- a/content/docs/references/billing-prior-to-2026-06-01.mdx +++ b/content/docs/references/billing-prior-to-2026-06-01.mdx @@ -1,4 +1,7 @@ --- +related: + - /references/billing + - /references/rate-limits title: Billing Policy For Usage Prior to 2026-06-01 description: Billing rules that applied to usage on or before May 31, 2026. lifecycle: deprecated diff --git a/content/docs/references/billing.mdx b/content/docs/references/billing.mdx index 6bdbbce..9ee788b 100644 --- a/content/docs/references/billing.mdx +++ b/content/docs/references/billing.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/rate-limits + - /references/account/get_account_usage_data + - /documentation/purchasing-api + - /references/billing-prior-to-2026-06-01 title: Billing description: Understand how API usage is tracked and billed. icon: CreditCard diff --git a/content/docs/references/events/get_event.mdx b/content/docs/references/events/get_event.mdx index 184c513..8cf6d30 100644 --- a/content/docs/references/events/get_event.mdx +++ b/content/docs/references/events/get_event.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/events/get-event + - /references/events/search_deed_events + - /references/authentication lifecycle: current topic: deed-events keywords: diff --git a/content/docs/references/events/search_deed_events.mdx b/content/docs/references/events/search_deed_events.mdx index 9fa0cb1..2de3c61 100644 --- a/content/docs/references/events/search_deed_events.mdx +++ b/content/docs/references/events/search_deed_events.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/events/search-events + - /references/events/get_event + - /references/regions/list_counties + - /references/authentication title: Search deed events description: Search historical deed events by region, date range, and property identifier. full: true diff --git a/content/docs/references/events/search_events.mdx b/content/docs/references/events/search_events.mdx index 62a91da..98a924c 100644 --- a/content/docs/references/events/search_events.mdx +++ b/content/docs/references/events/search_events.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/events/search-events + - /references/events/get_event + - /references/authentication lifecycle: current topic: deed-events keywords: diff --git a/content/docs/references/migration/index.mdx b/content/docs/references/migration/index.mdx index 9e1e123..d9ea840 100644 --- a/content/docs/references/migration/index.mdx +++ b/content/docs/references/migration/index.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/migration/person-v1-to-v2 + - /references/migration/property-v1-to-v2 + - /references/versioning/breaking-changes-policy + - /changelog title: Migration Guides description: Move from V1 endpoints to V2 with field-level mappings and call translations. icon: GitMerge diff --git a/content/docs/references/migration/person-v1-to-v2.mdx b/content/docs/references/migration/person-v1-to-v2.mdx index fabdc34..e4123b2 100644 --- a/content/docs/references/migration/person-v1-to-v2.mdx +++ b/content/docs/references/migration/person-v1-to-v2.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /references/person-v2/get_person_by_id_v2 + - /references/migration + - /references/versioning/breaking-changes-policy title: Person V1 → V2 description: Field mapping and behavior changes when moving from /v1/person to /v2/person. icon: User diff --git a/content/docs/references/migration/property-v1-to-v2.mdx b/content/docs/references/migration/property-v1-to-v2.mdx index 51d0f6d..06125bc 100644 --- a/content/docs/references/migration/property-v1-to-v2.mdx +++ b/content/docs/references/migration/property-v1-to-v2.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/property-v2/search_property_v2 + - /references/property-v2/get_property_by_id_v2 + - /references/migration + - /references/versioning/breaking-changes-policy title: Property V1 → V2 description: Field mapping and behavior changes when moving from /v1/property to /v2/property. icon: House diff --git a/content/docs/references/person-v2/get_person_by_id_v2.mdx b/content/docs/references/person-v2/get_person_by_id_v2.mdx index e02f7c6..c4995b5 100644 --- a/content/docs/references/person-v2/get_person_by_id_v2.mdx +++ b/content/docs/references/person-v2/get_person_by_id_v2.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/person-v2/search_person_by_name_phone_or_address_v2 + - /documentation/agentic-guidance/entity-shapes + - /documentation/person-search + - /references/authentication lifecycle: current topic: person keywords: diff --git a/content/docs/references/person-v2/search_person_by_name_phone_or_address_v2.mdx b/content/docs/references/person-v2/search_person_by_name_phone_or_address_v2.mdx index 466fcc6..1f094af 100644 --- a/content/docs/references/person-v2/search_person_by_name_phone_or_address_v2.mdx +++ b/content/docs/references/person-v2/search_person_by_name_phone_or_address_v2.mdx @@ -1,4 +1,10 @@ --- +related: + - /documentation/person-search + - /documentation/agentic-guidance/capability-map + - /references/person-v2/get_person_by_id_v2 + - /references/authentication + - /references/rate-limits lifecycle: current topic: person keywords: diff --git a/content/docs/references/property-v2/get_property_by_id_v2.mdx b/content/docs/references/property-v2/get_property_by_id_v2.mdx index db45a2a..2159350 100644 --- a/content/docs/references/property-v2/get_property_by_id_v2.mdx +++ b/content/docs/references/property-v2/get_property_by_id_v2.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/property-v2/search_property_v2 + - /documentation/agentic-guidance/entity-shapes + - /documentation/property-search + - /references/authentication lifecycle: current topic: property keywords: diff --git a/content/docs/references/property-v2/search_property_v2.mdx b/content/docs/references/property-v2/search_property_v2.mdx index 4ad656f..2087683 100644 --- a/content/docs/references/property-v2/search_property_v2.mdx +++ b/content/docs/references/property-v2/search_property_v2.mdx @@ -1,4 +1,10 @@ --- +related: + - /documentation/property-search + - /documentation/agentic-guidance/capability-map + - /references/property-v2/get_property_by_id_v2 + - /references/authentication + - /references/rate-limits lifecycle: current topic: property keywords: diff --git a/content/docs/references/rate-limits.mdx b/content/docs/references/rate-limits.mdx index c50b3b8..fcae339 100644 --- a/content/docs/references/rate-limits.mdx +++ b/content/docs/references/rate-limits.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/billing + - /references/account/get_account_usage_data + - /documentation/purchasing-api + - /references/authentication title: Rate Limits description: Understand the rate limiting policies for the Whitepages API, including monthly query limits and trial exhaustion. icon: Gauge diff --git a/content/docs/references/regions/list_counties.mdx b/content/docs/references/regions/list_counties.mdx index b4f08fc..cccbed6 100644 --- a/content/docs/references/regions/list_counties.mdx +++ b/content/docs/references/regions/list_counties.mdx @@ -1,4 +1,9 @@ --- +related: + - /references/regions/list_states + - /documentation/regions + - /references/events/search_deed_events + - /references/authentication lifecycle: current keywords: - "list counties" diff --git a/content/docs/references/regions/list_states.mdx b/content/docs/references/regions/list_states.mdx index 1598213..52c1c57 100644 --- a/content/docs/references/regions/list_states.mdx +++ b/content/docs/references/regions/list_states.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/regions/list_counties + - /documentation/regions + - /references/authentication lifecycle: current keywords: - "list states" diff --git a/content/docs/references/support-and-incident-response.mdx b/content/docs/references/support-and-incident-response.mdx index b9d4411..6e64f48 100644 --- a/content/docs/references/support-and-incident-response.mdx +++ b/content/docs/references/support-and-incident-response.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/versioning/breaking-changes-policy + - /references/versioning/change-notifications + - /references/rate-limits title: Support & Incident Response description: Learn about our support channels, incident response times, and how to get help during an outage. icon: Shield diff --git a/content/docs/references/versioning/breaking-changes-policy.mdx b/content/docs/references/versioning/breaking-changes-policy.mdx index b702f90..30b017f 100644 --- a/content/docs/references/versioning/breaking-changes-policy.mdx +++ b/content/docs/references/versioning/breaking-changes-policy.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/versioning/change-notifications + - /references/migration + - /references/support-and-incident-response title: Breaking Changes Policy description: How we define, announce, and version breaking changes across the REST API and the MCP server. icon: GitBranch diff --git a/content/docs/references/versioning/change-notifications.mdx b/content/docs/references/versioning/change-notifications.mdx index e31c691..be1ec8d 100644 --- a/content/docs/references/versioning/change-notifications.mdx +++ b/content/docs/references/versioning/change-notifications.mdx @@ -1,4 +1,8 @@ --- +related: + - /references/versioning/breaking-changes-policy + - /changelog + - /references/support-and-incident-response title: Change Notifications description: How we notify you about breaking changes, deprecations, new features, and maintenance. icon: Bell diff --git a/content/docs/references/webhooks/create_webhook.mdx b/content/docs/references/webhooks/create_webhook.mdx index 164f695..60a2e87 100644 --- a/content/docs/references/webhooks/create_webhook.mdx +++ b/content/docs/references/webhooks/create_webhook.mdx @@ -1,4 +1,9 @@ --- +related: + - /documentation/webhooks/create-webhook + - /references/webhooks/list_webhooks + - /references/regions/list_counties + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/content/docs/references/webhooks/delete_webhook.mdx b/content/docs/references/webhooks/delete_webhook.mdx index a7de727..27e18ca 100644 --- a/content/docs/references/webhooks/delete_webhook.mdx +++ b/content/docs/references/webhooks/delete_webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/delete-webhook + - /references/webhooks/list_webhooks + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/content/docs/references/webhooks/get_webhook.mdx b/content/docs/references/webhooks/get_webhook.mdx index 0f55673..76cd2c3 100644 --- a/content/docs/references/webhooks/get_webhook.mdx +++ b/content/docs/references/webhooks/get_webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/list-webhooks + - /references/webhooks/list_webhooks + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/content/docs/references/webhooks/list_webhooks.mdx b/content/docs/references/webhooks/list_webhooks.mdx index d7682ba..ea11f0d 100644 --- a/content/docs/references/webhooks/list_webhooks.mdx +++ b/content/docs/references/webhooks/list_webhooks.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/list-webhooks + - /references/webhooks/get_webhook + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/content/docs/references/webhooks/test_webhook.mdx b/content/docs/references/webhooks/test_webhook.mdx index 41fdbc9..2f3b502 100644 --- a/content/docs/references/webhooks/test_webhook.mdx +++ b/content/docs/references/webhooks/test_webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/test-webhook + - /references/webhooks/create_webhook + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/content/docs/references/webhooks/update_webhook.mdx b/content/docs/references/webhooks/update_webhook.mdx index 1dfb841..b6f70e1 100644 --- a/content/docs/references/webhooks/update_webhook.mdx +++ b/content/docs/references/webhooks/update_webhook.mdx @@ -1,4 +1,8 @@ --- +related: + - /documentation/webhooks/update-webhook + - /references/webhooks/get_webhook + - /references/authentication lifecycle: current topic: webhooks keywords: diff --git a/scripts/generators/corpus/corpus.core.test.ts b/scripts/generators/corpus/corpus.core.test.ts index c59d57e..531ad40 100644 --- a/scripts/generators/corpus/corpus.core.test.ts +++ b/scripts/generators/corpus/corpus.core.test.ts @@ -3,13 +3,26 @@ import { assertCorpusInvariants, buildCatalog, cleanBody, + deriveDescription, deriveId, + deriveKind, fileToSlug, + hasApiPage, + isNavShell, parseDoc, + parseEndpoint, + parseRelated, + renderRelated, + resolveInternalPath, + resolveRelated, + rewriteBody, serializeCatalog, splitFrontmatter, + summarize, toBody, toCatalogEntry, + topicForPath, + withRelated, } from "./corpus.core"; describe("splitFrontmatter", () => { @@ -50,11 +63,37 @@ describe("deriveId", () => { expect(deriveId("")).toBe("index"); }); - test("flattens slashes into underscores", () => { - expect(deriveId("references/versioning/breaking-changes-policy")).toBe( - "references_versioning_breaking-changes-policy", + test("normalizes both slashes and underscores to dashes", () => { + expect(deriveId("references/account/get_account_usage_data")).toBe( + "references-account-get-account-usage-data", ); }); + + test("leaves an already-dashed slug untouched", () => { + expect(deriveId("references/person-v2")).toBe("references-person-v2"); + }); +}); + +describe("isNavShell", () => { + test.each([ + ["", true], + ["references", true], + ["documentation/guides", true], + ["documentation/person-search", false], + ["references/authentication", false], + ])("isNavShell(%p) === %p", (slug, expected) => { + expect(isNavShell(slug)).toBe(expected); + }); +}); + +describe("hasApiPage", () => { + test("detects an APIPage block", () => { + expect(hasApiPage("text\n")).toBe(true); + }); + + test("ignores prose mentioning the word", () => { + expect(hasApiPage("The APIPageView component is unrelated.")).toBe(false); + }); }); describe("cleanBody", () => { @@ -64,6 +103,17 @@ describe("cleanBody", () => { expect(cleanBody(body)).toBe("text"); }); + test("strips MDX comments", () => { + const body = "{/* This file was generated. Do not edit. */}\n\ntext"; + expect(cleanBody(body)).toBe("text"); + }); + + test("strips a self-closing APIPage block", () => { + const body = + ''; + expect(cleanBody(body)).toBe(""); + }); + test("keeps prose that merely mentions import and from", () => { const body = "You can import records from your account."; expect(cleanBody(body)).toBe(body); @@ -78,27 +128,316 @@ describe("toBody", () => { test("prepends the title as an H1 and ends with a newline", () => { expect(toBody("Title", "content")).toBe("# Title\n\ncontent\n"); }); + + test("emits a title-only body when the content is empty", () => { + expect(toBody("Title", "")).toBe("# Title\n"); + }); +}); + +describe("summarize", () => { + test("returns a short single sentence unchanged", () => { + expect(summarize("Retrieve person information.")).toBe( + "Retrieve person information.", + ); + }); + + test("collapses whitespace and keeps the first paragraph", () => { + expect(summarize("First line.\n\nSecond paragraph.")).toBe("First line."); + }); + + test("reduces a long paragraph to its first sentence", () => { + const long = `${"a".repeat(10)}. ${"b".repeat(250)}`; + expect(summarize(long)).toBe(`${"a".repeat(10)}.`); + }); + + test("hard-truncates when even the first sentence is too long", () => { + const result = summarize("x".repeat(400)); + expect(result.length).toBe(200); + expect(result.endsWith("…")).toBe(true); + }); +}); + +describe("deriveDescription", () => { + test("prefers the frontmatter description", () => { + expect( + deriveDescription({ description: "From frontmatter." }, "body"), + ).toBe("From frontmatter."); + }); + + test("falls back to summary then to the body lead", () => { + expect(deriveDescription({ summary: "From summary." }, "body")).toBe( + "From summary.", + ); + expect(deriveDescription({}, "Lead sentence. More.")).toBe( + "Lead sentence. More.", + ); + }); + + test("ignores the H1 heading when reading the body lead", () => { + expect(deriveDescription({}, "# Title\n\nReal lead.")).toBe("Real lead."); + }); +}); + +describe("topicForPath", () => { + test.each([ + ["documentation/events/event-payload", "deed-events"], + ["references/events/search_deed_events", "deed-events"], + ["documentation/webhooks/create-webhook", "webhooks"], + ["references/regions/list_states", "regions"], + ["documentation/regions", "regions"], + ["references/versioning/breaking-changes-policy", "versioning"], + ["references/migration/person-v1-to-v2", "person"], + ["changelog", "versioning"], + ["documentation/purchasing-api", "account-billing"], + ["references/support-and-incident-response", "account-billing"], + ["documentation/getting-started", "getting-started"], + ["references/person-v2/get_person_by_id_v2", "person"], + ])("topicForPath(%p) === %p", (slug, expected) => { + expect(topicForPath(slug)).toBe(expected); + }); + + test("returns null for an uncategorized slug", () => { + expect(topicForPath("some-other-root")).toBe(null); + }); +}); + +describe("deriveKind", () => { + test("endpoint when an APIPage is present", () => { + expect(deriveKind("references/person-v2/get_person_by_id_v2", true)).toBe( + "endpoint", + ); + }); + + test("reference for non-endpoint pages under references", () => { + expect(deriveKind("references/authentication", false)).toBe("reference"); + }); + + test("guide for everything else", () => { + expect(deriveKind("documentation/getting-started", false)).toBe("guide"); + }); +}); + +describe("parseEndpoint", () => { + test("derives the operationId from the slug and path/method from the body", () => { + const body = + ''; + expect( + parseEndpoint("references/person-v2/get_person_by_id_v2", body), + ).toEqual({ + operationId: "get_person_by_id_v2", + path: "/v2/person/{id}", + method: "get", + }); + }); + + test("throws when the operation cannot be parsed", () => { + expect(() => parseEndpoint("references/x", "")).toThrow( + /no parseable operation/, + ); + }); +}); + +describe("resolveInternalPath", () => { + const slugToId = new Map([ + ["references/authentication", "references-authentication"], + ["documentation/person-search", "documentation-person-search"], + ]); + + test("resolves an internal path to a known id, ignoring anchors", () => { + expect( + resolveInternalPath("/references/authentication#keys", slugToId), + ).toBe("references-authentication"); + }); + + test("returns null for external links", () => { + expect(resolveInternalPath("https://example.com", slugToId)).toBe(null); + }); + + test("returns null for an internal path with no matching doc", () => { + expect(resolveInternalPath("/references", slugToId)).toBe(null); + }); +}); + +describe("rewriteBody", () => { + const slugToId = new Map([ + ["references/billing", "references-billing"], + ["documentation/person-search", "documentation-person-search"], + ]); + + test("rewrites markdown links and collects sorted unique edges", () => { + const body = + "See [billing](/references/billing) and [people](/documentation/person-search) and [again](/references/billing)."; + const { body: rewritten, edges } = rewriteBody(body, slugToId, "self"); + + expect(rewritten).toContain("[billing](doc_id:references-billing)"); + expect(rewritten).toContain("[people](doc_id:documentation-person-search)"); + expect(edges).toEqual([ + "documentation-person-search", + "references-billing", + ]); + }); + + test("rewrites JSX href attributes, including the braced form", () => { + const body = + ''; + const { body: rewritten, edges } = rewriteBody(body, slugToId, "self"); + + expect(rewritten).toContain('href="doc_id:references-billing"'); + expect(rewritten).toContain('href={"doc_id:references-billing"}'); + expect(edges).toEqual(["references-billing"]); + }); + + test("leaves links to dropped or unknown targets untouched", () => { + const body = "See [routes](/references) and [ext](https://x.com)."; + const { body: rewritten, edges } = rewriteBody(body, slugToId, "self"); + + expect(rewritten).toBe(body); + expect(edges).toEqual([]); + }); + + test("does not create a self-edge", () => { + const body = "[here](/references/billing)"; + const { edges } = rewriteBody(body, slugToId, "references-billing"); + + expect(edges).toEqual([]); + }); +}); + +describe("parseRelated", () => { + test("reads a string list and ignores non-strings", () => { + expect( + parseRelated({ + related: ["/references/billing", 1, "/references/rate-limits"], + }), + ).toEqual(["/references/billing", "/references/rate-limits"]); + }); + + test("returns an empty list when related is absent", () => { + expect(parseRelated({})).toEqual([]); + }); +}); + +describe("resolveRelated", () => { + const slugToId = new Map([ + ["references/billing", "references-billing"], + ["references/rate-limits", "references-rate-limits"], + ]); + + test("resolves related URLs to deduplicated doc ids", () => { + expect( + resolveRelated( + "self", + [ + "/references/billing", + "/references/rate-limits", + "/references/billing", + ], + slugToId, + ), + ).toEqual(["references-billing", "references-rate-limits"]); + }); + + test("throws on an unresolvable path", () => { + expect(() => + resolveRelated("self", ["/references/nope"], slugToId), + ).toThrow(/unresolvable path/); + }); + + test("throws when a doc relates to itself", () => { + expect(() => + resolveRelated("references-billing", ["/references/billing"], slugToId), + ).toThrow(/itself/); + }); +}); + +describe("renderRelated", () => { + test("renders a Related section of doc_id links", () => { + expect( + renderRelated([ + { id: "references-billing", title: "Billing" }, + { id: "references-rate-limits", title: "Rate Limits" }, + ]), + ).toBe( + "## Related\n\n- [Billing](doc_id:references-billing)\n- [Rate Limits](doc_id:references-rate-limits)", + ); + }); + + test("renders nothing for an empty list", () => { + expect(renderRelated([])).toBe(""); + }); +}); + +describe("withRelated", () => { + test("appends the section with a blank-line separator", () => { + expect(withRelated("# Title\n", "## Related\n\n- x")).toBe( + "# Title\n\n## Related\n\n- x\n", + ); + }); + + test("returns the body unchanged when there is no section", () => { + expect(withRelated("# Title\n", "")).toBe("# Title\n"); + }); }); describe("toCatalogEntry", () => { - test("applies defaults for missing facets", () => { - expect(toCatalogEntry("x", { title: "X" })).toEqual({ - id: "x", - title: "X", - description: "", + test("builds a guide entry with derived facets and defaults", () => { + const entry = toCatalogEntry({ + id: "documentation-getting-started", + slug: "documentation/getting-started", + data: { title: "Getting Started", description: "Get started." }, + cleanedBody: "Get started.", + rawBody: "Get started.", + isEndpoint: false, + }); + + expect(entry).toEqual({ + id: "documentation-getting-started", + title: "Getting Started", + description: "Get started.", + kind: "guide", lifecycle: "current", - topic: null, + topic: "getting-started", keywords: [], + edges: [], + endpoint: null, + }); + }); + + test("builds an endpoint entry with the endpoint facet", () => { + const rawBody = + ''; + const entry = toCatalogEntry({ + id: "references-person-v2-search-person", + slug: "references/person-v2/search_person", + data: { title: "Search", description: "Find a person." }, + cleanedBody: "", + rawBody, + isEndpoint: true, + }); + + expect(entry.kind).toBe("endpoint"); + expect(entry.topic).toBe("person"); + expect(entry.endpoint).toEqual({ + operationId: "search_person", + path: "/v2/person/", + method: "get", }); }); - test("reads deprecated lifecycle, topic, and string keywords", () => { - const entry = toCatalogEntry("x", { - title: "X", - description: "d", - lifecycle: "deprecated", - topic: "person", - keywords: ["a", 1, "b"], + test("reads deprecated lifecycle and string keywords, frontmatter topic wins", () => { + const entry = toCatalogEntry({ + id: "x", + slug: "documentation/agentic-guidance/entity-shapes", + data: { + title: "X", + description: "d", + lifecycle: "deprecated", + topic: "person", + keywords: ["a", 1, "b"], + }, + cleanedBody: "d", + rawBody: "d", + isEndpoint: false, }); expect(entry.lifecycle).toBe("deprecated"); @@ -106,28 +445,81 @@ describe("toCatalogEntry", () => { expect(entry.keywords).toEqual(["a", "b"]); }); - test("falls back to the id when title is missing", () => { - expect(toCatalogEntry("the-id", {}).title).toBe("the-id"); + test("rejects an unknown topic and falls back to the path", () => { + const entry = toCatalogEntry({ + id: "x", + slug: "references/authentication", + data: { title: "X", topic: "not-a-topic" }, + cleanedBody: "body", + rawBody: "body", + isEndpoint: false, + }); + + expect(entry.topic).toBe("account-billing"); }); - test("rejects an unknown topic", () => { - expect( - toCatalogEntry("x", { title: "X", topic: "not-a-topic" }).topic, - ).toBe(null); + test("falls back to the id when title is missing", () => { + const entry = toCatalogEntry({ + id: "the-id", + slug: "the-id", + data: {}, + cleanedBody: "", + rawBody: "", + isEndpoint: false, + }); + + expect(entry.title).toBe("the-id"); }); }); describe("parseDoc", () => { - test("parses raw MDX into id, catalog entry, and body", () => { + test("parses a guide into id, slug, catalog entry, and body", () => { const raw = "---\ntitle: Rate Limits\ntopic: account-billing\n---\n\nBody text."; const doc = parseDoc("references/rate-limits.mdx", raw); - expect(doc.id).toBe("references_rate-limits"); - expect(doc.entry.title).toBe("Rate Limits"); + expect(doc.id).toBe("references-rate-limits"); + expect(doc.slug).toBe("references/rate-limits"); + expect(doc.entry.kind).toBe("reference"); expect(doc.entry.topic).toBe("account-billing"); expect(doc.body).toBe("# Rate Limits\n\nBody text.\n"); }); + + test("strips the APIPage body but keeps the endpoint facet", () => { + const raw = `--- +title: Search +description: Find a person. +--- + +{/* generated */} + +`; + const doc = parseDoc( + "references/person-v2/search_person_by_name_phone_or_address_v2.mdx", + raw, + ); + + expect(doc.body).toBe("# Search\n"); + expect(doc.entry.endpoint).toEqual({ + operationId: "search_person_by_name_phone_or_address_v2", + path: "/v2/person/", + method: "get", + }); + }); + + test("extracts related URLs from frontmatter", () => { + const raw = + "---\ntitle: X\nrelated:\n - /references/billing\n - /references/rate-limits\n---\n\nBody."; + const doc = parseDoc("references/x.mdx", raw); + + expect(doc.related).toEqual([ + "/references/billing", + "/references/rate-limits", + ]); + }); }); describe("assertCorpusInvariants", () => { @@ -145,23 +537,27 @@ describe("assertCorpusInvariants", () => { }); describe("buildCatalog", () => { + const entry = (id: string) => + toCatalogEntry({ + id, + slug: id, + data: { title: id.toUpperCase() }, + cleanedBody: "", + rawBody: "", + isEndpoint: false, + }); + test("sorts docs by id and sets the version", () => { - const catalog = buildCatalog( - [ - toCatalogEntry("b", { title: "B" }), - toCatalogEntry("a", { title: "A" }), - ], - 1, - ); + const catalog = buildCatalog([entry("b"), entry("a")], 2); - expect(catalog.version).toBe(1); + expect(catalog.version).toBe(2); expect(catalog.docs.map((doc) => doc.id)).toEqual(["a", "b"]); }); }); describe("serializeCatalog", () => { test("produces indented JSON with a trailing newline", () => { - const out = serializeCatalog({ version: 1, docs: [] }); - expect(out).toBe('{\n "version": 1,\n "docs": []\n}\n'); + const out = serializeCatalog({ version: 2, docs: [] }); + expect(out).toBe('{\n "version": 2,\n "docs": []\n}\n'); }); }); diff --git a/scripts/generators/corpus/corpus.core.ts b/scripts/generators/corpus/corpus.core.ts index 4530504..0a3116e 100644 --- a/scripts/generators/corpus/corpus.core.ts +++ b/scripts/generators/corpus/corpus.core.ts @@ -2,13 +2,22 @@ import { parse as parseYaml } from "yaml"; import type { Catalog, CatalogEntry, + DocKind, + EndpointFacet, Lifecycle, ParsedDoc, Topic, } from "./corpus.types"; -export const CATALOG_VERSION = 1; +export const CATALOG_VERSION = 2; export const RESERVED_IDS: readonly string[] = ["everything", "openapi"]; +export const NAV_SHELL_SLUGS: readonly string[] = [ + "", + "references", + "documentation/guides", +]; + +const DESCRIPTION_MAX_LENGTH = 200; const TOPICS = new Set([ "person", @@ -17,6 +26,8 @@ const TOPICS = new Set([ "webhooks", "account-billing", "getting-started", + "regions", + "versioning", ]); function isTopic(value: unknown): value is Topic { @@ -26,6 +37,12 @@ function isTopic(value: unknown): value is Topic { const FRONTMATTER_PATTERN = /^---\n([\s\S]*?)\n---\n?/; const IMPORT_EXPORT_LINE = /^\s*(?:import|export)\s.+\sfrom\s+["'][^"']+["'];?\s*$/; +const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g; +const API_PAGE_BLOCK = //g; +const API_PAGE_OPERATION = + /path:\s*["']([^"']+)["']\s*,\s*method:\s*["']([^"']+)["']/; +const MARKDOWN_LINK = /\]\((\/[^)\s]+)\)/g; +const JSX_HREF = /href=(\{?)["'](\/[^"'\s]+)["'](\}?)/g; export function splitFrontmatter(raw: string): { data: Record; @@ -57,11 +74,21 @@ export function fileToSlug(relativePath: string): string { } export function deriveId(slug: string): string { - return slug === "" ? "index" : slug.replaceAll("/", "_"); + return slug === "" ? "index" : slug.replaceAll("/", "-").replaceAll("_", "-"); +} + +export function isNavShell(slug: string): boolean { + return NAV_SHELL_SLUGS.includes(slug); +} + +export function hasApiPage(rawBody: string): boolean { + return /]/.test(rawBody); } export function cleanBody(body: string): string { return body + .replace(MDX_COMMENT, "") + .replace(API_PAGE_BLOCK, "") .split("\n") .filter((line) => !IMPORT_EXPORT_LINE.test(line)) .join("\n") @@ -69,18 +96,225 @@ export function cleanBody(body: string): string { .trim(); } -export function toBody(title: string, rawBody: string): string { - return `# ${title}\n\n${cleanBody(rawBody)}\n`; +export function toBody(title: string, cleanedBody: string): string { + return cleanedBody ? `# ${title}\n\n${cleanedBody}\n` : `# ${title}\n`; } function asString(value: unknown, fallback: string): string { return typeof value === "string" ? value : fallback; } -export function toCatalogEntry( - id: string, +export function summarize(text: string): string { + const firstParagraph = text.split(/\n\s*\n/)[0] ?? ""; + const collapsed = firstParagraph.replace(/\s+/g, " ").trim(); + + if (collapsed.length <= DESCRIPTION_MAX_LENGTH) { + return collapsed; + } + + const sentence = collapsed.match(/^.*?[.!?](?=\s|$)/)?.[0]?.trim(); + + if (sentence && sentence.length <= DESCRIPTION_MAX_LENGTH) { + return sentence; + } + + return `${collapsed.slice(0, DESCRIPTION_MAX_LENGTH - 1).trimEnd()}…`; +} + +export function deriveDescription( data: Record, -): CatalogEntry { + cleanedBody: string, +): string { + const explicit = + asString(data.description, "").trim() || asString(data.summary, "").trim(); + const bodyText = cleanedBody.replace(/^#\s.*\n+/, ""); + return summarize(explicit || bodyText); +} + +export function topicForPath(slug: string): Topic | null { + if ( + slug.startsWith("documentation/person-search") || + slug.startsWith("references/person-v2") || + slug.includes("person-v1-to-v2") + ) { + return "person"; + } + + if ( + slug.startsWith("documentation/property-search") || + slug.startsWith("references/property-v2") || + slug.includes("property-v1-to-v2") + ) { + return "property"; + } + + if ( + slug.startsWith("documentation/events") || + slug.startsWith("references/events") + ) { + return "deed-events"; + } + + if ( + slug.startsWith("documentation/webhooks") || + slug.startsWith("references/webhooks") + ) { + return "webhooks"; + } + + if ( + slug === "documentation/regions" || + slug.startsWith("references/regions") + ) { + return "regions"; + } + + if ( + slug === "changelog" || + slug.startsWith("references/versioning") || + slug.startsWith("references/migration") + ) { + return "versioning"; + } + + if ( + slug === "documentation/purchasing-api" || + slug.startsWith("references/") + ) { + return "account-billing"; + } + + if (slug.startsWith("documentation/")) { + return "getting-started"; + } + + return null; +} + +export function deriveKind(slug: string, isEndpoint: boolean): DocKind { + if (isEndpoint) { + return "endpoint"; + } + + return slug.startsWith("references/") ? "reference" : "guide"; +} + +export function parseEndpoint(slug: string, rawBody: string): EndpointFacet { + const match = rawBody.match(API_PAGE_OPERATION); + + if (!match) { + throw new Error( + `Endpoint doc "${slug}" has an with no parseable operation`, + ); + } + + const operationId = slug.split("/").pop() ?? slug; + return { operationId, path: match[1], method: match[2].toLowerCase() }; +} + +export function resolveInternalPath( + target: string, + slugToId: ReadonlyMap, +): string | null { + const withoutAnchor = target.split("#")[0] ?? ""; + + if (!withoutAnchor.startsWith("/")) { + return null; + } + + const slug = withoutAnchor.replace(/^\/+/, "").replace(/\/+$/, ""); + return slugToId.get(slug) ?? null; +} + +export function rewriteBody( + body: string, + slugToId: ReadonlyMap, + selfId: string, +): { body: string; edges: readonly string[] } { + const edges = new Set(); + + const rewriteTarget = (target: string): string | null => { + const id = resolveInternalPath(target, slugToId); + + if (id === null || id === selfId) { + return null; + } + + edges.add(id); + return `doc_id:${id}`; + }; + + const withMarkdown = body.replace(MARKDOWN_LINK, (whole, target: string) => { + const docId = rewriteTarget(target); + return docId ? `](${docId})` : whole; + }); + + const rewritten = withMarkdown.replace( + JSX_HREF, + (whole, open: string, target: string, close: string) => { + const docId = rewriteTarget(target); + return docId ? `href=${open}"${docId}"${close}` : whole; + }, + ); + + return { body: rewritten, edges: [...edges].sort() }; +} + +export function parseRelated(data: Record): readonly string[] { + return Array.isArray(data.related) + ? data.related.filter((entry): entry is string => typeof entry === "string") + : []; +} + +export function resolveRelated( + docId: string, + relatedPaths: readonly string[], + slugToId: ReadonlyMap, +): readonly string[] { + const seen = new Set(); + + for (const path of relatedPaths) { + const id = resolveInternalPath(path, slugToId); + + if (id === null) { + throw new Error(`Doc "${docId}" relates to unresolvable path "${path}"`); + } + + if (id === docId) { + throw new Error(`Doc "${docId}" lists itself in related`); + } + + seen.add(id); + } + + return [...seen]; +} + +export function renderRelated( + items: readonly { id: string; title: string }[], +): string { + if (items.length === 0) { + return ""; + } + + const links = items.map((item) => `- [${item.title}](doc_id:${item.id})`); + return `## Related\n\n${links.join("\n")}`; +} + +export function withRelated(body: string, section: string): string { + return section ? `${body.trimEnd()}\n\n${section}\n` : body; +} + +export function toCatalogEntry(input: { + id: string; + slug: string; + data: Record; + cleanedBody: string; + rawBody: string; + isEndpoint: boolean; +}): CatalogEntry { + const { id, slug, data, cleanedBody, rawBody, isEndpoint } = input; + const lifecycle: Lifecycle = data.lifecycle === "deprecated" ? "deprecated" : "current"; @@ -93,10 +327,13 @@ export function toCatalogEntry( return { id, title: asString(data.title, id), - description: asString(data.description, ""), + description: deriveDescription(data, cleanedBody), + kind: deriveKind(slug, isEndpoint), lifecycle, - topic: isTopic(data.topic) ? data.topic : null, + topic: isTopic(data.topic) ? data.topic : topicForPath(slug), keywords, + edges: [], + endpoint: isEndpoint ? parseEndpoint(slug, rawBody) : null, }; } @@ -129,9 +366,25 @@ export function serializeCatalog(catalog: Catalog): string { } export function parseDoc(relativePath: string, raw: string): ParsedDoc { - const { data, body } = splitFrontmatter(raw); - const id = deriveId(fileToSlug(relativePath)); - const entry = toCatalogEntry(id, data); + const { data, body: rawBody } = splitFrontmatter(raw); + const slug = fileToSlug(relativePath); + const id = deriveId(slug); + const isEndpoint = hasApiPage(rawBody); + const cleanedBody = cleanBody(rawBody); + const entry = toCatalogEntry({ + id, + slug, + data, + cleanedBody, + rawBody, + isEndpoint, + }); - return { id, entry, body: toBody(entry.title, body) }; + return { + id, + slug, + entry, + body: toBody(entry.title, cleanedBody), + related: parseRelated(data), + }; } diff --git a/scripts/generators/corpus/corpus.generator.ts b/scripts/generators/corpus/corpus.generator.ts index 0527ca2..b0507be 100644 --- a/scripts/generators/corpus/corpus.generator.ts +++ b/scripts/generators/corpus/corpus.generator.ts @@ -4,8 +4,13 @@ import { join } from "node:path"; import { assertCorpusInvariants, buildCatalog, + isNavShell, parseDoc, + renderRelated, + resolveRelated, + rewriteBody, serializeCatalog, + withRelated, } from "./corpus.core"; import type { ParsedDoc } from "./corpus.types"; @@ -17,9 +22,32 @@ async function readDoc(relativePath: string): Promise { return parseDoc(relativePath, raw); } +function linkDocs(docs: readonly ParsedDoc[]): readonly ParsedDoc[] { + const slugToId = new Map(docs.map((doc) => [doc.slug, doc.id])); + const idToTitle = new Map(docs.map((doc) => [doc.id, doc.entry.title])); + + return docs.map((doc) => { + const { body, edges: linkEdges } = rewriteBody(doc.body, slugToId, doc.id); + const related = resolveRelated(doc.id, doc.related, slugToId); + const section = renderRelated( + related.map((id) => ({ id, title: idToTitle.get(id) ?? id })), + ); + const edges = [...new Set([...linkEdges, ...related])].sort(); + + return { + ...doc, + body: withRelated(body, section), + entry: { ...doc.entry, edges }, + }; + }); +} + async function main(): Promise { const relativePaths = [...new Glob("**/*.mdx").scanSync(CONTENT_DIR)].sort(); - const docs = await Promise.all(relativePaths.map(readDoc)); + const parsed = await Promise.all(relativePaths.map(readDoc)); + const kept = parsed.filter((doc) => !isNavShell(doc.slug)); + + const docs = linkDocs(kept); assertCorpusInvariants(docs.map((doc) => doc.id)); @@ -34,7 +62,10 @@ async function main(): Promise { await writeFile(join(OUTPUT_DIR, "catalog.json"), serializeCatalog(catalog)); - console.log(`Wrote ${docs.length} docs + catalog.json to ${OUTPUT_DIR}`); + console.log( + `Wrote ${docs.length} docs + catalog.json to ${OUTPUT_DIR} ` + + `(dropped ${parsed.length - kept.length} nav-shell pages)`, + ); } await main(); diff --git a/scripts/generators/corpus/corpus.types.ts b/scripts/generators/corpus/corpus.types.ts index 5da2caa..7ee7a21 100644 --- a/scripts/generators/corpus/corpus.types.ts +++ b/scripts/generators/corpus/corpus.types.ts @@ -6,15 +6,28 @@ export type Topic = | "deed-events" | "webhooks" | "account-billing" - | "getting-started"; + | "getting-started" + | "regions" + | "versioning"; + +export type DocKind = "guide" | "reference" | "endpoint"; + +export interface EndpointFacet { + readonly operationId: string; + readonly path: string; + readonly method: string; +} export interface CatalogEntry { readonly id: string; readonly title: string; readonly description: string; + readonly kind: DocKind; readonly lifecycle: Lifecycle; readonly topic: Topic | null; readonly keywords: readonly string[]; + readonly edges: readonly string[]; + readonly endpoint: EndpointFacet | null; } export interface Catalog { @@ -24,6 +37,8 @@ export interface Catalog { export interface ParsedDoc { readonly id: string; + readonly slug: string; readonly entry: CatalogEntry; readonly body: string; + readonly related: readonly string[]; } diff --git a/scripts/generators/openapi/openapi.facets.test.ts b/scripts/generators/openapi/openapi.facets.test.ts index c7164c9..6cea284 100644 --- a/scripts/generators/openapi/openapi.facets.test.ts +++ b/scripts/generators/openapi/openapi.facets.test.ts @@ -36,6 +36,15 @@ describe("resolveFacets", () => { expect(facets.topic).toBe("person"); expect(facets.keywords).toEqual([]); + expect(facets.related).toEqual([]); + }); + + test("resolves curated related URLs for a known endpoint", () => { + const facets = resolveFacets("person-v2/get_person_by_id_v2.mdx"); + + expect(facets.related).toContain( + "/references/person-v2/search_person_by_name_phone_or_address_v2", + ); }); }); @@ -47,6 +56,7 @@ describe("injectFacets", () => { lifecycle: "current", topic: "person", keywords: ["a", "b"], + related: [], }); expect(out).toBe( @@ -54,10 +64,23 @@ describe("injectFacets", () => { ); }); + test("emits the related block first, with unquoted URLs", () => { + const out = injectFacets(sample, { + lifecycle: "current", + keywords: [], + related: ["/references/billing", "/references/rate-limits"], + }); + + expect(out).toBe( + "---\nrelated:\n - /references/billing\n - /references/rate-limits\nlifecycle: current\ntitle: X\ndescription: Y\n---\n\nbody", + ); + }); + test("omits the topic line when topic is undefined", () => { const out = injectFacets(sample, { lifecycle: "current", keywords: ["a"], + related: [], }); expect(out).not.toContain("topic:"); @@ -69,22 +92,38 @@ describe("injectFacets", () => { lifecycle: "current", topic: "person", keywords: [], + related: [], }); expect(out).not.toContain("keywords:"); }); + test("omits the related block when there are no related ids", () => { + const out = injectFacets(sample, { + lifecycle: "current", + keywords: [], + related: [], + }); + + expect(out).not.toContain("related:"); + }); + test("quotes keywords so reserved characters stay valid YAML", () => { const out = injectFacets(sample, { lifecycle: "current", keywords: ["who owns this: property"], + related: [], }); expect(out).toContain(' - "who owns this: property"'); }); test("preserves existing frontmatter and body", () => { - const out = injectFacets(sample, { lifecycle: "current", keywords: [] }); + const out = injectFacets(sample, { + lifecycle: "current", + keywords: [], + related: [], + }); expect(out).toContain("title: X"); expect(out).toContain("description: Y"); @@ -95,7 +134,11 @@ describe("injectFacets", () => { const noFrontmatter = "no frontmatter here"; expect( - injectFacets(noFrontmatter, { lifecycle: "current", keywords: ["a"] }), + injectFacets(noFrontmatter, { + lifecycle: "current", + keywords: ["a"], + related: [], + }), ).toBe(noFrontmatter); }); }); diff --git a/scripts/generators/openapi/openapi.facets.ts b/scripts/generators/openapi/openapi.facets.ts index 559cde3..621220a 100644 --- a/scripts/generators/openapi/openapi.facets.ts +++ b/scripts/generators/openapi/openapi.facets.ts @@ -4,6 +4,7 @@ export interface Facets { lifecycle: Lifecycle; topic?: Topic; keywords: string[]; + related: string[]; } const DIRECTORY_TOPICS: Record = { @@ -74,6 +75,98 @@ const FILE_KEYWORDS: Record = { "regions/list_counties.mdx": ["list counties", "counties by state"], }; +const FILE_RELATED: Record = { + "person-v2/search_person_by_name_phone_or_address_v2.mdx": [ + "/documentation/person-search", + "/documentation/agentic-guidance/capability-map", + "/references/person-v2/get_person_by_id_v2", + "/references/authentication", + "/references/rate-limits", + ], + "person-v2/get_person_by_id_v2.mdx": [ + "/references/person-v2/search_person_by_name_phone_or_address_v2", + "/documentation/agentic-guidance/entity-shapes", + "/documentation/person-search", + "/references/authentication", + ], + "property-v2/search_property_v2.mdx": [ + "/documentation/property-search", + "/documentation/agentic-guidance/capability-map", + "/references/property-v2/get_property_by_id_v2", + "/references/authentication", + "/references/rate-limits", + ], + "property-v2/get_property_by_id_v2.mdx": [ + "/references/property-v2/search_property_v2", + "/documentation/agentic-guidance/entity-shapes", + "/documentation/property-search", + "/references/authentication", + ], + "events/get_event.mdx": [ + "/documentation/events/get-event", + "/references/events/search_deed_events", + "/references/authentication", + ], + "events/search_deed_events.mdx": [ + "/documentation/events/search-events", + "/references/events/get_event", + "/references/regions/list_counties", + "/references/authentication", + ], + "events/search_events.mdx": [ + "/documentation/events/search-events", + "/references/events/get_event", + "/references/authentication", + ], + "webhooks/create_webhook.mdx": [ + "/documentation/webhooks/create-webhook", + "/references/webhooks/list_webhooks", + "/references/regions/list_counties", + "/references/authentication", + ], + "webhooks/delete_webhook.mdx": [ + "/documentation/webhooks/delete-webhook", + "/references/webhooks/list_webhooks", + "/references/authentication", + ], + "webhooks/get_webhook.mdx": [ + "/documentation/webhooks/list-webhooks", + "/references/webhooks/list_webhooks", + "/references/authentication", + ], + "webhooks/list_webhooks.mdx": [ + "/documentation/webhooks/list-webhooks", + "/references/webhooks/get_webhook", + "/references/authentication", + ], + "webhooks/test_webhook.mdx": [ + "/documentation/webhooks/test-webhook", + "/references/webhooks/create_webhook", + "/references/authentication", + ], + "webhooks/update_webhook.mdx": [ + "/documentation/webhooks/update-webhook", + "/references/webhooks/get_webhook", + "/references/authentication", + ], + "account/get_account_usage_data.mdx": [ + "/references/billing", + "/references/rate-limits", + "/references/authentication", + ], + "regions/list_states.mdx": [ + "/references/regions/list_counties", + "/documentation/regions", + "/references/authentication", + ], + "regions/list_counties.mdx": [ + "/references/regions/list_states", + "/documentation/regions", + "/references/events/search_deed_events", + "/references/authentication", + ], +}; + function topicForPath(filePath: string): Topic | undefined { const directory = filePath.split("/")[0]; return DIRECTORY_TOPICS[directory]; @@ -84,11 +177,21 @@ export function resolveFacets(filePath: string): Facets { lifecycle: "current", topic: topicForPath(filePath), keywords: FILE_KEYWORDS[filePath] ?? [], + related: FILE_RELATED[filePath] ?? [], }; } function serializeFacets(facets: Facets): string { - const lines = [`lifecycle: ${facets.lifecycle}`]; + const lines: string[] = []; + + if (facets.related.length > 0) { + lines.push("related:"); + for (const id of facets.related) { + lines.push(` - ${id}`); + } + } + + lines.push(`lifecycle: ${facets.lifecycle}`); if (facets.topic) { lines.push(`topic: ${facets.topic}`); diff --git a/src/app/[[...slug]]/page.tsx b/src/app/[[...slug]]/page.tsx index 1c326b6..719a20a 100644 --- a/src/app/[[...slug]]/page.tsx +++ b/src/app/[[...slug]]/page.tsx @@ -9,6 +9,21 @@ import { notFound } from "next/navigation"; import { getMDXComponents } from "@/mdx-components"; import type { Metadata } from "next"; import { createRelativeLink } from "fumadocs-ui/mdx"; +import { RelatedLinks, type RelatedLink } from "@/components/related-links"; + +function resolveRelatedLinks(urls: readonly string[]): RelatedLink[] { + return urls.flatMap((url) => { + const target = source.getPage(url.split("/").filter(Boolean)); + if (!target) return []; + return [ + { + url: target.url, + title: target.data.title, + description: target.data.description, + }, + ]; + }); +} export default async function Page(props: PageProps<"/[[...slug]]">) { const params = await props.params; @@ -16,6 +31,7 @@ export default async function Page(props: PageProps<"/[[...slug]]">) { if (!page) notFound(); const MDX = page.data.body; + const relatedLinks = resolveRelatedLinks(page.data.related); return ( @@ -27,6 +43,7 @@ export default async function Page(props: PageProps<"/[[...slug]]">) { a: createRelativeLink(source, page), })} /> + ); diff --git a/src/components/related-links.tsx b/src/components/related-links.tsx new file mode 100644 index 0000000..3dbce99 --- /dev/null +++ b/src/components/related-links.tsx @@ -0,0 +1,33 @@ +import { Card, Cards } from "fumadocs-ui/components/card"; + +export interface RelatedLink { + readonly url: string; + readonly title: string; + readonly description?: string; +} + +export function RelatedLinks({ + links, +}: { + links: readonly RelatedLink[]; +}): React.ReactNode { + if (links.length === 0) { + return null; + } + + return ( + <> +

Related

+ + {links.map((link) => ( + + ))} + + + ); +} diff --git a/src/lib/facets.ts b/src/lib/facets.ts index 1e24088..d4487f7 100644 --- a/src/lib/facets.ts +++ b/src/lib/facets.ts @@ -17,4 +17,5 @@ export const facetFrontmatterFields = { lifecycle: z.enum(LIFECYCLES).default("current"), topic: z.enum(TOPICS).optional(), keywords: z.array(z.string()).default([]), + related: z.array(z.string()).default([]), }; From 82063490fba2b72310cc8f5c0cc90abd5d26c64a Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Wed, 24 Jun 2026 21:50:54 -0400 Subject: [PATCH 2/3] feat: emit a taxonomy block in the corpus catalog The generator now owns the facet vocabularies. catalog.json carries a taxonomy of the distinct topics, kinds, and lifecycles present, each as a value, a human label, and a document count, so consumers read it instead of reconstructing the vocabularies and labels themselves. Manifest contract change: bump the catalog version to 3. --- scripts/generators/corpus/corpus.core.test.ts | 65 ++++++++++++++++++- scripts/generators/corpus/corpus.core.ts | 61 ++++++++++++++++- scripts/generators/corpus/corpus.types.ts | 13 ++++ 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/scripts/generators/corpus/corpus.core.test.ts b/scripts/generators/corpus/corpus.core.test.ts index 531ad40..04099a5 100644 --- a/scripts/generators/corpus/corpus.core.test.ts +++ b/scripts/generators/corpus/corpus.core.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { assertCorpusInvariants, buildCatalog, + buildTaxonomy, cleanBody, deriveDescription, deriveId, @@ -547,17 +548,75 @@ describe("buildCatalog", () => { isEndpoint: false, }); - test("sorts docs by id and sets the version", () => { + test("sorts docs by id, sets the version, and embeds the taxonomy", () => { const catalog = buildCatalog([entry("b"), entry("a")], 2); expect(catalog.version).toBe(2); expect(catalog.docs.map((doc) => doc.id)).toEqual(["a", "b"]); + expect(catalog.taxonomy.kinds).toEqual([ + { value: "guide", label: "Guide", count: 2 }, + ]); + }); +}); + +describe("buildTaxonomy", () => { + const guidePerson = toCatalogEntry({ + id: "a", + slug: "documentation/person-search/x", + data: { title: "A" }, + cleanedBody: "", + rawBody: "", + isEndpoint: false, + }); + const refBilling = toCatalogEntry({ + id: "b", + slug: "references/billing", + data: { title: "B", lifecycle: "deprecated" }, + cleanedBody: "", + rawBody: "", + isEndpoint: false, + }); + + test("counts distinct topics, kinds, and lifecycles with labels", () => { + const taxonomy = buildTaxonomy([guidePerson, refBilling]); + + expect(taxonomy.topics).toEqual([ + { value: "account-billing", label: "Account & Billing", count: 1 }, + { value: "person", label: "Person", count: 1 }, + ]); + expect(taxonomy.kinds).toEqual([ + { value: "guide", label: "Guide", count: 1 }, + { value: "reference", label: "Reference", count: 1 }, + ]); + expect(taxonomy.lifecycles).toEqual([ + { value: "current", label: "Current", count: 1 }, + { value: "deprecated", label: "Deprecated", count: 1 }, + ]); + }); + + test("excludes docs with a null topic", () => { + const uncategorized = toCatalogEntry({ + id: "u", + slug: "floating-root", + data: { title: "U" }, + cleanedBody: "", + rawBody: "", + isEndpoint: false, + }); + + expect(buildTaxonomy([uncategorized]).topics).toEqual([]); }); }); describe("serializeCatalog", () => { test("produces indented JSON with a trailing newline", () => { - const out = serializeCatalog({ version: 2, docs: [] }); - expect(out).toBe('{\n "version": 2,\n "docs": []\n}\n'); + const out = serializeCatalog({ + version: 3, + taxonomy: { topics: [], kinds: [], lifecycles: [] }, + docs: [], + }); + expect(out).toBe( + '{\n "version": 3,\n "taxonomy": {\n "topics": [],\n "kinds": [],\n "lifecycles": []\n },\n "docs": []\n}\n', + ); }); }); diff --git a/scripts/generators/corpus/corpus.core.ts b/scripts/generators/corpus/corpus.core.ts index 0a3116e..cf7580d 100644 --- a/scripts/generators/corpus/corpus.core.ts +++ b/scripts/generators/corpus/corpus.core.ts @@ -6,10 +6,12 @@ import type { EndpointFacet, Lifecycle, ParsedDoc, + Taxonomy, + TaxonomyTerm, Topic, } from "./corpus.types"; -export const CATALOG_VERSION = 2; +export const CATALOG_VERSION = 3; export const RESERVED_IDS: readonly string[] = ["everything", "openapi"]; export const NAV_SHELL_SLUGS: readonly string[] = [ "", @@ -353,12 +355,67 @@ export function assertCorpusInvariants(ids: readonly string[]): void { } } +const TOPIC_LABELS: Record = { + person: "Person", + property: "Property", + "deed-events": "Deed Events", + webhooks: "Webhooks", + "account-billing": "Account & Billing", + "getting-started": "Getting Started", + regions: "Regions", + versioning: "Versioning", +}; + +const KIND_LABELS: Record = { + guide: "Guide", + reference: "Reference", + endpoint: "Endpoint", +}; + +const LIFECYCLE_LABELS: Record = { + current: "Current", + deprecated: "Deprecated", +}; + +function countTerms( + values: readonly T[], + labels: Record, +): readonly TaxonomyTerm[] { + const counts = new Map(); + + for (const value of values) { + counts.set(value, (counts.get(value) ?? 0) + 1); + } + + return [...counts.entries()] + .map(([value, count]) => ({ value, label: labels[value], count })) + .sort((a, b) => a.value.localeCompare(b.value)); +} + +export function buildTaxonomy(entries: readonly CatalogEntry[]): Taxonomy { + const topics = entries + .map((entry) => entry.topic) + .filter((topic): topic is Topic => topic !== null); + + return { + topics: countTerms(topics, TOPIC_LABELS), + kinds: countTerms( + entries.map((entry) => entry.kind), + KIND_LABELS, + ), + lifecycles: countTerms( + entries.map((entry) => entry.lifecycle), + LIFECYCLE_LABELS, + ), + }; +} + export function buildCatalog( entries: readonly CatalogEntry[], version: number = CATALOG_VERSION, ): Catalog { const docs = [...entries].sort((a, b) => a.id.localeCompare(b.id)); - return { version, docs }; + return { version, taxonomy: buildTaxonomy(docs), docs }; } export function serializeCatalog(catalog: Catalog): string { diff --git a/scripts/generators/corpus/corpus.types.ts b/scripts/generators/corpus/corpus.types.ts index 7ee7a21..75d3ff7 100644 --- a/scripts/generators/corpus/corpus.types.ts +++ b/scripts/generators/corpus/corpus.types.ts @@ -30,8 +30,21 @@ export interface CatalogEntry { readonly endpoint: EndpointFacet | null; } +export interface TaxonomyTerm { + readonly value: string; + readonly label: string; + readonly count: number; +} + +export interface Taxonomy { + readonly topics: readonly TaxonomyTerm[]; + readonly kinds: readonly TaxonomyTerm[]; + readonly lifecycles: readonly TaxonomyTerm[]; +} + export interface Catalog { readonly version: number; + readonly taxonomy: Taxonomy; readonly docs: readonly CatalogEntry[]; } From 51a0a50f07afb60370c9afedb6940f4a23d648fb Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Thu, 25 Jun 2026 11:05:18 -0400 Subject: [PATCH 3/3] docs: require related links when adding documentation pages Rename CLAUDE.md to AGENTS.md as the canonical agent-instructions file and leave CLAUDE.md as a symlink to it. Document that every new page under content/docs must declare a related list of site-relative URLs. These power the Related cards on the docs site and the per-document edges in the generated corpus, and generated endpoint pages curate them in the openapi facets generator. --- AGENTS.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 111 +--------------------------------------------- 2 files changed, 130 insertions(+), 110 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0da1713 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. + +## Code Style + +- Don't abbreviate names in code (e.g., use 'navigation' instead of 'nav') +- Avoid comments in code. If code needs a comment to be understood, refactor it to be more readable instead. + +## Documentation Pages + +When adding a new page under `content/docs/`: + +- Add frontmatter with `title` and `description`, plus the facets that apply (`lifecycle`, `topic`, `keywords`). +- Include a `related:` list of the most relevant other pages, as site-relative URLs — the further reading a reader (or agent) would want next: + + ```yaml + related: + - /references/authentication + - /documentation/agentic-guidance/capability-map + ``` + + This is required, not optional polish. `related` drives two things: the "Related" cards rendered at the bottom of the page on the docs site, and the per-document `edges` in the generated corpus that power related-content discovery for the MCP documentation tools. A page with no `related` links is a dead end in that graph. + +- Keep the list curated — a few genuinely relevant neighbors, not every page in the section. Point at sibling guides, the matching endpoint reference, and the operational pages (authentication, billing, rate limits) a reader will need next. +- Use URLs (e.g. `/references/billing`), never corpus ids. The generator resolves each URL to its corpus slug; the docs site links it directly. An unresolvable URL fails the corpus build. +- For generated endpoint pages under `content/docs/references/`, curate related links in `scripts/generators/openapi/openapi.facets.ts` (`FILE_RELATED`), not in the generated `.mdx` files — `bun run generate:openapi` overwrites those. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 16f2e50..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,110 +0,0 @@ -Default to using Bun instead of Node.js. - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; - -// import .css files directly and it works -import './index.css'; - -import { createRoot } from "react-dom/client"; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. - -## Code Style - -- Don't abbreviate names in code (e.g., use 'navigation' instead of 'nav') -- Avoid comments in code. If code needs a comment to be understood, refactor it to be more readable instead. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file