From b6af174608993065abdb67f7b3fc861e0d83d13a Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Thu, 3 Sep 2026 16:29:38 +0700 Subject: [PATCH 1/7] Capability map: source, generator, JSON route, readable page, domain bundles content/capabilities.yaml is the hand-authored index; generate-capability-map.mjs validates it against the stable Admin API spec (operations by tag or id, webhook events), the content tree (developer pages), and the skills table, then writes lib/generated/capabilities.json and the generated /docs/capabilities page. Served at /capabilities.json (schema at /capabilities.schema.json) and rendered into six plain-Markdown bundles at /llms/.txt: capability records for one domain followed by the full text of the developer pages they cite, with MDX components converted to Markdown. llms.txt lists the map and the bundles before the full corpus. Frontmatter gains audience, capability_ids, status, last_verified. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 2 + app/capabilities.json/route.ts | 14 + app/docs/layout.tsx | 7 +- app/llms.txt/route.ts | 11 +- app/llms/admin-api.txt/route.ts | 5 + app/llms/apps-webhooks.txt/route.ts | 5 + app/llms/campaigns.txt/route.ts | 5 + app/llms/payments.txt/route.ts | 5 + app/llms/platform.txt/route.ts | 5 + app/llms/storefront.txt/route.ts | 5 + content/capabilities.yaml | 414 ++++++++++++++++++++++++++++ content/docs/capabilities/meta.json | 1 + content/docs/meta.json | 12 +- lib/bundles.ts | 108 ++++++++ lib/capabilities.ts | 84 ++++++ lib/plain-text.ts | 81 ++++++ package.json | 11 +- public/capabilities.schema.json | 93 +++++++ scripts/generate-capability-map.mjs | 376 +++++++++++++++++++++++++ source.config.ts | 10 + 20 files changed, 1248 insertions(+), 6 deletions(-) create mode 100644 app/capabilities.json/route.ts create mode 100644 app/llms/admin-api.txt/route.ts create mode 100644 app/llms/apps-webhooks.txt/route.ts create mode 100644 app/llms/campaigns.txt/route.ts create mode 100644 app/llms/payments.txt/route.ts create mode 100644 app/llms/platform.txt/route.ts create mode 100644 app/llms/storefront.txt/route.ts create mode 100644 content/capabilities.yaml create mode 100644 content/docs/capabilities/meta.json create mode 100644 lib/bundles.ts create mode 100644 lib/capabilities.ts create mode 100644 lib/plain-text.ts create mode 100644 public/capabilities.schema.json create mode 100644 scripts/generate-capability-map.mjs diff --git a/.gitignore b/.gitignore index 39c052bd..ab61d9bf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ out/ /content/docs/storefront/graphql/mutations/ /content/docs/campaigns/api/*/ /lib/generated/ +# Generated capability page (regenerated by npm run generate; source is content/capabilities.yaml) +/content/docs/capabilities/index.mdx # Misc .DS_Store diff --git a/app/capabilities.json/route.ts b/app/capabilities.json/route.ts new file mode 100644 index 00000000..27200dc2 --- /dev/null +++ b/app/capabilities.json/route.ts @@ -0,0 +1,14 @@ +import { capabilityMap } from '@/lib/capabilities'; + +export const revalidate = false; + +/** + * The platform capability map, generated at build time by + * scripts/generate-capability-map.mjs from content/capabilities.yaml. + * Stable URL: https://developers.nextcommerce.com/capabilities.json + */ +export function GET() { + return new Response(JSON.stringify(capabilityMap, null, 2) + '\n', { + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }); +} diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index 18f1e4c6..ebba16b9 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -3,7 +3,7 @@ import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import { AlgoliaDocSearch, AlgoliaDocSearchMobile } from '@/components/search'; import type { ReactNode } from 'react'; -import { ChevronsLeftRightEllipsis, Megaphone, ShoppingBag, Puzzle, Webhook, Sparkles, FlaskConical } from 'lucide-react'; +import { ChevronsLeftRightEllipsis, Megaphone, ShoppingBag, Puzzle, Webhook, Sparkles, FlaskConical, Map } from 'lucide-react'; import type { SidebarTab } from 'fumadocs-ui/utils/get-sidebar-tabs'; const sectionMeta: Record = { @@ -42,6 +42,11 @@ const sectionMeta: Record, + description: 'Platform capability map', + color: 'bg-slate-500/15 text-slate-500', + }, }; export default function Layout({ children }: { children: ReactNode }) { diff --git a/app/llms.txt/route.ts b/app/llms.txt/route.ts index 16172b56..4659197c 100644 --- a/app/llms.txt/route.ts +++ b/app/llms.txt/route.ts @@ -1,4 +1,5 @@ import { source } from '@/lib/source'; +import { capabilityMap } from '@/lib/capabilities'; export const revalidate = false; @@ -65,7 +66,8 @@ function header(): string { `- [Merchant docs](${MERCHANT_SITE}): guides for store operators, on the sibling site`, `- [Merchant docs index](${MERCHANT_SITE}/llms.txt)`, `- [Platform and API changelog](${MERCHANT_SITE}/changelog): the developer portal has no changelog of its own`, - `- [Full corpus](${SITE}/llms-full.txt): every page in one file (large, about 1.5 MB)`, + `- [Capability map (JSON)](${SITE}/capabilities.json): one record per platform capability linking merchant guides, developer guides, Admin API operations, webhook events, and skills under a stable id`, + `- [Capability map (readable)](${SITE}/docs/capabilities)`, `- [Admin API spec, 2024-04-01](${SITE}/api/admin/2024-04-01.yaml): stable, raw OpenAPI`, `- [Admin API spec, unstable](${SITE}/api/admin/unstable.yaml): raw OpenAPI`, `- [Admin API spec, 2023-02-10](${SITE}/api/admin/2023-02-10.yaml): deprecated, raw OpenAPI`, @@ -75,6 +77,13 @@ function header(): string { `- [Testing](${SITE}/docs/testing)`, `- [Agent guide (AGENTS.md)](https://github.com/NextCommerceCo/developer-docs/blob/main/AGENTS.md): navigation and evidence rules for agents reading this site`, '', + '## Domain bundles', + '', + 'Fetch the bundle for your question before the full corpus. Each is plain Markdown: the capability records for one domain followed by the full text of the developer pages they cite.', + '', + ...capabilityMap.bundles.map((b) => `- [${b.title}](${b.url}): ${b.intro}`), + `- [Full corpus](${SITE}/llms-full.txt): every page in one file, including 500+ generated reference pages (large, about 1.5 MB); use a bundle or a page URL instead unless you need everything`, + '', '## Legacy identifiers', '', 'Next Commerce was formerly 29 Next. Hostnames like `{store}.29next.store`, `accounts.29next.com`, and headers like `X-29next-API-Version` and `X-29Next-Signature` are current, valid technical identifiers and must be used exactly as written.', diff --git a/app/llms/admin-api.txt/route.ts b/app/llms/admin-api.txt/route.ts new file mode 100644 index 00000000..233c9aca --- /dev/null +++ b/app/llms/admin-api.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('admin-api'); diff --git a/app/llms/apps-webhooks.txt/route.ts b/app/llms/apps-webhooks.txt/route.ts new file mode 100644 index 00000000..7e327b00 --- /dev/null +++ b/app/llms/apps-webhooks.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('apps-webhooks'); diff --git a/app/llms/campaigns.txt/route.ts b/app/llms/campaigns.txt/route.ts new file mode 100644 index 00000000..045425fc --- /dev/null +++ b/app/llms/campaigns.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('campaigns'); diff --git a/app/llms/payments.txt/route.ts b/app/llms/payments.txt/route.ts new file mode 100644 index 00000000..46781db8 --- /dev/null +++ b/app/llms/payments.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('payments'); diff --git a/app/llms/platform.txt/route.ts b/app/llms/platform.txt/route.ts new file mode 100644 index 00000000..1e6dcc66 --- /dev/null +++ b/app/llms/platform.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('platform'); diff --git a/app/llms/storefront.txt/route.ts b/app/llms/storefront.txt/route.ts new file mode 100644 index 00000000..142a8ac8 --- /dev/null +++ b/app/llms/storefront.txt/route.ts @@ -0,0 +1,5 @@ +import { bundleResponse } from '@/lib/bundles'; + +export const revalidate = false; + +export const GET = bundleResponse('storefront'); diff --git a/content/capabilities.yaml b/content/capabilities.yaml new file mode 100644 index 00000000..88e010df --- /dev/null +++ b/content/capabilities.yaml @@ -0,0 +1,414 @@ +# Platform capability map: the hand-authored source of the projection served at +# https://developers.nextcommerce.com/capabilities.json +# +# This file is an index over existing sources, not a third source of product +# truth. The owning sources win whenever they disagree with it: +# - prose -> the linked merchant (docs.nextcommerce.com) and developer pages +# - operations -> public/api/admin/.yaml (resolved here by tag or operationId) +# - webhooks -> the `webhooks` block of the same spec (resolved and validated here) +# - skills -> content/docs/skills/index.mdx (validated here) +# - currency -> https://docs.nextcommerce.com/changelog +# +# scripts/generate-capability-map.mjs validates every field and writes +# lib/generated/capabilities.json. The build fails on an unknown page, operation, +# event, skill, bundle, or enum value. +# +# Record fields +# id stable, kebab-case; consumed by page frontmatter (capability_ids) on both sites +# title human name +# summary one to three sentences of plain prose an agent can quote +# audiences subset of [merchant, developer] +# operator_docs merchant-site paths (absolute URLs are generated) +# developer_docs developer-site paths; must exist under content/docs after generation +# api_operations "tag:" selectors and/or operationIds from the stable Admin API spec +# webhooks event names from the spec, or the single word "all" +# skills skill names from the AI Skills page +# status available | beta | deprecated +# last_verified YYYY-MM-DD when a person last checked every link in the record +# notes optional caveats an agent must repeat (rates, legacy names, missing events) + +version: 1 +spec_version: "2024-04-01" + +bundles: + - id: platform + title: Platform overview + intro: >- + What Next Commerce is, how the two documentation sites divide the material, the + Admin API versions, how to test safely, and the legacy identifiers an agent must + not "correct". Start here before fetching a domain bundle. + capabilities: [admin-api, testing, legacy-identifiers, agent-skills] + - id: admin-api + title: Admin API + intro: >- + The REST Admin API and its guides: orders and external checkout, subscriptions, + fulfillment, and exports. Operations are listed by tag; the versioned OpenAPI file + is the contract. + capabilities: [admin-api, orders, subscriptions, fulfillment] + - id: payments + title: Payments and gateways + intro: >- + Gateways, payment methods, gateway selection on API-created orders, disputes, + and the Test Gateway. NEXT Payments processing rates are not published. + capabilities: [payments-gateways, disputes, testing] + - id: campaigns + title: Campaigns + intro: >- + Campaign funnels: the Campaigns API, Campaign Page Kit, starter templates, and the + Admin API surface for campaigns and offers. Campaign orders are store orders. + capabilities: [campaigns] + - id: storefront + title: Storefront + intro: >- + Storefront themes and Theme Kit, template objects, tags and filters, theme settings, + the storefront GraphQL API, checkout links, and event tracking. + capabilities: [storefront-themes, checkout-links] + - id: apps-webhooks + title: Apps and webhooks + intro: >- + Building apps with OAuth, app manifests and settings, service integrations + (fulfillment, disputes, attribution), and every webhook event with its payload. + capabilities: [apps, webhooks] + +capabilities: + - id: admin-api + title: Admin API + summary: >- + The REST Admin API manages store resources: products, orders, customers, + subscriptions, fulfillment, payments, campaigns, and more. Access is by OAuth app + token against https://{store}.29next.store/api/admin/, with the version selected per + request by the X-29next-API-Version header. 2024-04-01 is the stable version; + 2023-02-10 is deprecated; unstable carries in-progress changes. + audiences: [developer] + operator_docs: + - /docs/build-a-store/technical-settings/configure-webhooks + - /changelog + developer_docs: + - /docs/admin-api + - /docs/admin-api/permissions + - /docs/admin-api/guides/exports + api_operations: [tag:store, tag:exports, tag:metadata] + webhooks: [export.created, store.updated] + skills: [next-ops-scan] + status: available + last_verified: 2026-09-03 + notes: + - The developer portal has no changelog of its own; version history is on the merchant changelog. + - There is no consolidated migration guide between API versions yet. + + - id: orders + title: Orders and external checkout + summary: >- + Orders can be created through the Admin API from an external checkout (cart, order, + upsell flow) and managed afterwards: fulfil, refund, capture, cancel, and edit. + Campaign and storefront orders land in the same orders list. + audiences: [merchant, developer] + operator_docs: + - /docs/manage/orders/collect-payments-on-orders + - /docs/manage/orders/test-orders + developer_docs: + - /docs/admin-api/guides/external-checkout + - /docs/admin-api/guides/order-management + api_operations: [tag:orders, tag:carts] + webhooks: [order.created, order.updated, cart.abandoned] + skills: [] + status: available + last_verified: 2026-09-03 + + - id: subscriptions + title: Subscriptions + summary: >- + Native recurring orders. Merchants configure phases, statuses, pause, cancellation + paths, decline salvage, and account updater in the dashboard; developers create and + manage subscriptions through the Admin API. Renewals are not a separate webhook: + a renewal surfaces as transaction.created (and order.created) with billing_cycle set. + audiences: [merchant, developer] + operator_docs: + - /docs/manage/subscriptions-guide + - /docs/manage/subscriptions-guide/managing-subscriptions + - /docs/manage/subscriptions-guide/subscription-phases + - /docs/manage/subscriptions-guide/subscription-statuses + - /docs/manage/subscriptions-guide/subscription-settings + - /docs/manage/subscriptions-guide/pause-subscriptions + - /docs/manage/subscriptions-guide/cancellation-paths + - /docs/manage/subscriptions-guide/decline-salvage + - /docs/manage/subscriptions-guide/account-updater + - /docs/analytics/subscription-performance + developer_docs: + - /docs/admin-api/guides/subscription-management + api_operations: [tag:subscriptions] + webhooks: [subscription.created, subscription.updated, transaction.created] + skills: [next-bulk-subscription] + status: available + last_verified: 2026-09-03 + notes: + - There is no subscription.renewed event. Detect renewals from transaction.created with billing_cycle. + + - id: payments-gateways + title: Payments and gateways + summary: >- + Merchants add bankcard gateways and alternative payment methods under Settings > + Payments and group them for routing and failure cascading. Developers choose a + gateway on an API-created order with payment_details.payment_gateway or + payment_gateway_group, and integrate each payment method through its guide. + NEXT Payments is the platform's own processing service. + audiences: [merchant, developer] + operator_docs: + - /docs/features/payments + - /docs/start-here/get-started/add-payment-providers + - /docs/features/payments/gateways/next-payments + - /docs/features/payments/payment-failure-cascading + - /docs/features/payments/authorize-and-capture-payments + - /docs/features/payments/3ds2-payments + - /docs/features/payments/external-payment-methods + - /docs/features/payments/risk-screening + - /docs/features/payments/transaction-response-codes + developer_docs: + - /docs/admin-api/guides/payment-methods + - /docs/admin-api/guides/payment-methods/bankcard + - /docs/admin-api/guides/payment-methods/apple-pay + - /docs/admin-api/guides/payment-methods/google-pay + - /docs/admin-api/guides/payment-methods/paypal + - /docs/admin-api/guides/payment-methods/klarna + - /docs/admin-api/guides/payment-methods/affirm + - /docs/admin-api/guides/payment-methods/afterpay + - /docs/admin-api/guides/payment-methods/link + - /docs/admin-api/guides/payment-methods/bancontact + - /docs/admin-api/guides/payment-methods/ideal + - /docs/admin-api/guides/payment-methods/sepa-debit + - /docs/admin-api/guides/payment-methods/swish + - /docs/admin-api/guides/payment-methods/twint + api_operations: [tag:payments] + webhooks: [gateway.created, gateway.updated, transaction.created, transaction.updated] + skills: [] + status: available + last_verified: 2026-09-03 + notes: + - NEXT Payments processing rates are not published in the documentation; they are quoted by sales. + + - id: disputes + title: Disputes + summary: >- + Disputes are chargebacks. Merchants review and respond to them in the dashboard and + report on them; apps can act as a dispute service and receive dispute events. + audiences: [merchant, developer] + operator_docs: + - /docs/features/payments/disputes-guide + - /docs/analytics/disputes-reports + developer_docs: + - /docs/apps/guides/dispute-service + api_operations: [] + webhooks: [dispute.created, dispute.updated] + skills: [] + status: available + last_verified: 2026-09-03 + + - id: fulfillment + title: Fulfillment + summary: >- + Orders are fulfilled through locations, fulfillment statuses, and location-based + routing. Apps can register as a fulfillment service and receive assigned fulfillment + orders through the Admin API. + audiences: [merchant, developer] + operator_docs: + - /docs/features/fulfillment-guide + - /docs/features/fulfillment-guide/fulfillment-statuses + - /docs/features/fulfillment-guide/location-based-routing + - /docs/features/fulfillment-guide/advanced-settings + - /docs/start-here/get-started/fulfillment-settings + developer_docs: + - /docs/apps/guides/fulfillment-service + api_operations: [tag:fulfillment] + webhooks: [fulfillment.created, fulfillment.updated] + skills: [next-bulk-fulfill, next-bulk-move] + status: available + last_verified: 2026-09-03 + + - id: campaigns + title: Campaigns + summary: >- + Campaigns are custom checkout funnels (landing, checkout, upsell, receipt pages) + backed by the CORS-enabled Campaigns API, built with the Campaign Page Kit and + starter templates. A campaign's checkout is separate from the storefront checkout, + but its orders are ordinary store orders that share the orders list, inventory, + and subscriptions. + audiences: [merchant, developer] + operator_docs: + - /docs/apps/campaigns-app + - /docs/apps/campaigns-app/build-campaign-packages + - /docs/apps/campaigns-app/campaigns-offers-and-discounts + - /docs/apps/campaigns-app/campaign-analytics + developer_docs: + - /docs/campaigns + - /docs/campaigns/page-kit + - /docs/campaigns/templates + - /docs/campaigns/api + - /docs/campaigns/admin-api + api_operations: [tag:campaigns] + webhooks: [] + skills: [next-campaigns-setup] + status: available + last_verified: 2026-09-03 + + - id: storefront-themes + title: Storefront themes + summary: >- + The hosted storefront is rendered from a theme built with Django Template Language. + Merchants install and customise themes under Storefront > Themes; developers build + themes locally with Theme Kit (the ntk CLI: ntk pull, ntk push, ntk watch), starting + from the Spark starter theme, and query storefront data through the GraphQL API. + audiences: [merchant, developer] + operator_docs: + - /docs/build-a-store/storefront + - /docs/build-a-store/storefront/themes + - /docs/build-a-store/storefront/pages-and-assets + developer_docs: + - /docs/storefront + - /docs/storefront/themes + - /docs/storefront/themes/theme-kit + - /docs/storefront/themes/settings + - /docs/storefront/themes/translations + - /docs/storefront/themes/cdn-and-caching + - /docs/storefront/themes/templates + - /docs/storefront/themes/templates/objects + - /docs/storefront/themes/templates/tags + - /docs/storefront/themes/templates/filters + - /docs/storefront/themes/templates/urls-and-template-paths + - /docs/storefront/themes/guides/custom-page-templates + - /docs/storefront/themes/guides/custom-product-templates + - /docs/storefront/themes/guides/product-variants + - /docs/storefront/themes/guides/product-metadata + - /docs/storefront/themes/guides/personalized-products + - /docs/storefront/graphql + - /docs/storefront/event-tracking + api_operations: [tag:storefront] + webhooks: [] + skills: [next-theme-dev, next-theme-figma] + status: available + last_verified: 2026-09-03 + + - id: checkout-links + title: Checkout links + summary: >- + Prebuilt URLs that open the storefront checkout with products, quantities, and + attribution already applied, for use from ads, emails, and campaign pages. + audiences: [merchant, developer] + operator_docs: + - /docs/features/offers/shareable-coupon-links + developer_docs: + - /docs/storefront/checkout-links + api_operations: [] + webhooks: [] + skills: [] + status: available + last_verified: 2026-09-03 + + - id: apps + title: Apps and OAuth + summary: >- + Apps extend a store through OAuth, a manifest, settings, snippets, and storefront + extensions, and can act as fulfillment, dispute, or marketing-attribution services. + Merchants install apps from the dashboard; App Kit is the developer toolchain. + audiences: [developer] + operator_docs: [] + developer_docs: + - /docs/apps + - /docs/apps/app-development-flow + - /docs/apps/app-kit + - /docs/apps/manifest + - /docs/apps/settings + - /docs/apps/snippets + - /docs/apps/assets + - /docs/apps/review + - /docs/apps/event-tracking + - /docs/apps/oauth + - /docs/apps/oauth/getting-started + - /docs/apps/oauth/install-flows + - /docs/apps/oauth/session-auth + - /docs/apps/guides/server-to-server-apps + - /docs/apps/guides/storefront-extension + - /docs/apps/guides/marketing-attribution + api_operations: [tag:apps] + webhooks: [app.uninstalled] + skills: [] + status: available + last_verified: 2026-09-03 + notes: + - The merchant site documents individual apps (Klaviyo, ShipStation, Gorgias, and others) under /docs/apps/, not app installation in general. + + - id: webhooks + title: Webhooks + summary: >- + Stores send signed JSON webhooks for events on orders, customers, subscriptions, + transactions, disputes, fulfillment, products, gateways, tickets, exports, apps, + and the store itself. Merchants configure endpoints in the dashboard; developers + manage them through the Admin API and verify the X-29Next-Signature header. + audiences: [merchant, developer] + operator_docs: + - /docs/build-a-store/technical-settings/configure-webhooks + developer_docs: + - /docs/webhooks + api_operations: [tag:webhooks] + webhooks: all + skills: [] + status: available + last_verified: 2026-09-03 + + - id: testing + title: Testing and test orders + summary: >- + There is no separate sandbox. Test cards (6011111111111117, and 6011000990139424 for + 3DS) create tagged Test Orders on live stores without touching a gateway; the Test + Gateway produces realistic test transactions when an integration needs them. + audiences: [merchant, developer] + operator_docs: + - /docs/manage/orders/test-orders + - /docs/features/payments/gateways/test-gateway + developer_docs: + - /docs/testing + - /docs/admin-api/guides/testing-guide + api_operations: [] + webhooks: [] + skills: [] + status: available + last_verified: 2026-09-03 + notes: + - Do not tell a prospect there is no test mode; test cards work on live stores. + + - id: legacy-identifiers + title: Legacy identifiers (29next) + summary: >- + Next Commerce was formerly 29 Next, and the platform still carries that name in its + core technical identifiers: store and account hostnames ({store}.29next.store, + accounts.29next.com), the X-29next-API-Version and X-29Next-Signature headers, and + the API key namespace. These are current, in use on every store, and not scheduled + to change. Use them exactly as written. + audiences: [merchant, developer] + operator_docs: + - /docs/start-here/get-started + developer_docs: + - /docs/admin-api + - /docs/webhooks + api_operations: [] + webhooks: [] + skills: [] + status: available + last_verified: 2026-09-03 + notes: + - Never rewrite 29next identifiers to nextcommerce; the requests would fail. + + - id: agent-skills + title: AI agent skills + summary: >- + Pre-built skills that give AI coding agents platform knowledge (theme development, + campaign setup, bulk operations, daily ops scans), installable with the skills CLI + or loadable as plain markdown. + audiences: [developer] + operator_docs: [] + developer_docs: + - /docs/skills + api_operations: [] + webhooks: [] + skills: [next-theme-figma, next-theme-dev, next-campaigns-setup, next-bulk-fulfill, next-bulk-move, next-bulk-subscription, next-ops-scan] + status: available + last_verified: 2026-09-03 diff --git a/content/docs/capabilities/meta.json b/content/docs/capabilities/meta.json new file mode 100644 index 00000000..a11b05ff --- /dev/null +++ b/content/docs/capabilities/meta.json @@ -0,0 +1 @@ +{ "root": true, "title": "Capabilities", "icon": "Map", "description": "Platform capability map", "pages": ["index"] } diff --git a/content/docs/meta.json b/content/docs/meta.json index 530f3017..b227f45c 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,3 +1,13 @@ { - "pages": ["index", "admin-api", "campaigns", "storefront", "apps", "webhooks", "skills", "testing"] + "pages": [ + "index", + "admin-api", + "campaigns", + "storefront", + "apps", + "webhooks", + "skills", + "testing", + "capabilities" + ] } diff --git a/lib/bundles.ts b/lib/bundles.ts new file mode 100644 index 00000000..b29b6764 --- /dev/null +++ b/lib/bundles.ts @@ -0,0 +1,108 @@ +import { source } from '@/lib/source'; +import { toPlainMarkdown } from '@/lib/plain-text'; +import { + capabilityMap, + getBundle, + getCapability, + DEVELOPER_SITE, + type Capability, +} from '@/lib/capabilities'; + +/** + * Builds a domain bundle (/llms/.txt): the capability records in the bundle + * rendered as prose, followed by the plain-Markdown text of every developer page + * those records cite. Everything comes from the generated capability map and the + * page sources, so the bundle cannot say something the map and pages do not. + */ + +function absolutize(markdown: string): string { + return markdown.replaceAll('](/', `](${DEVELOPER_SITE}/`); +} + +function renderCapability(c: Capability): string { + const out: string[] = []; + out.push(`### ${c.title} (id: ${c.id})`, ''); + out.push(c.summary, ''); + out.push(`Status: ${c.status}. Audiences: ${c.audiences.join(', ')}. Links verified: ${c.last_verified}.`, ''); + if (c.notes.length > 0) { + out.push('Caveats:'); + for (const n of c.notes) out.push(`- ${n}`); + out.push(''); + } + if (c.operator_docs.length > 0) { + out.push('Merchant and operator guides (docs.nextcommerce.com):'); + for (const u of c.operator_docs) out.push(`- ${u}`); + out.push(''); + } + if (c.developer_docs.length > 0) { + out.push('Developer guides (developers.nextcommerce.com):'); + for (const u of c.developer_docs) out.push(`- ${u}`); + out.push(''); + } + if (c.api_operations.length > 0) { + out.push(`Admin API operations, version ${capabilityMap.sources.stable_api_version} (${c.api_operations.length}):`); + for (const op of c.api_operations) { + const text = `${op.method} ${op.path}${op.summary ? ` — ${op.summary}` : ''}`; + out.push(op.url ? `- [${text}](${op.url})` : `- ${text}`); + } + out.push(''); + } + if (c.webhooks.length > 0) { + out.push(`Webhook events (${c.webhooks.length}):`); + for (const w of c.webhooks) out.push(w.url ? `- [${w.event}](${w.url})` : `- ${w.event}`); + out.push(''); + } + if (c.skills.length > 0) { + out.push('AI agent skills:'); + for (const s of c.skills) out.push(`- [${s.name}](${s.url})`); + out.push(''); + } + return out.join('\n'); +} + +export async function buildBundle(id: string): Promise { + const bundle = getBundle(id); + if (!bundle) throw new Error(`Unknown bundle ${id}`); + const capabilities = bundle.capabilities + .map((cid) => getCapability(cid)) + .filter((c): c is Capability => Boolean(c)); + + const head = [ + `# Next Commerce: ${bundle.title}`, + '', + `> ${bundle.intro}`, + '', + `This is one of ${capabilityMap.bundles.length} domain bundles derived from the platform capability map at ${DEVELOPER_SITE}/capabilities.json (generated ${capabilityMap.generated_at}). The other bundles are listed at ${DEVELOPER_SITE}/llms.txt. Merchant and operator guides are on ${capabilityMap.sources.merchant_docs}; the changelog is ${capabilityMap.sources.changelog}. The Admin API contract is the OpenAPI file at ${capabilityMap.sources.admin_api_spec}.`, + '', + '## Capabilities', + '', + ...capabilities.map(renderCapability), + '## Pages', + '', + 'The full text of every developer page cited above, in the order listed.', + '', + ]; + + const seen = new Set(); + const pages: string[] = []; + for (const c of capabilities) { + for (const url of c.developer_docs) { + if (seen.has(url)) continue; + seen.add(url); + const slug = url.replace(`${DEVELOPER_SITE}/docs`, '').split('/').filter(Boolean); + const page = source.getPage(slug); + if (!page) continue; + const processed = await page.data.getText('processed'); + pages.push(`# ${page.data.title} (${url})`, '', absolutize(toPlainMarkdown(processed)), ''); + } + } + + return [...head, ...pages].join('\n'); +} + +export function bundleResponse(id: string): () => Promise { + return async () => + new Response(await buildBundle(id), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +} diff --git a/lib/capabilities.ts b/lib/capabilities.ts new file mode 100644 index 00000000..650130a2 --- /dev/null +++ b/lib/capabilities.ts @@ -0,0 +1,84 @@ +import generated from '@/lib/generated/capabilities.json'; + +/** + * Typed access to the generated capability map (lib/generated/capabilities.json, + * written by scripts/generate-capability-map.mjs from content/capabilities.yaml). + */ + +export interface CapabilityOperation { + id: string; + method: string; + path: string; + summary: string; + url: string | null; +} + +export interface CapabilityWebhook { + event: string; + url: string | null; +} + +export interface CapabilitySkill { + name: string; + url: string; +} + +export interface Capability { + id: string; + title: string; + summary: string; + audiences: string[]; + operator_docs: string[]; + developer_docs: string[]; + api_operations: CapabilityOperation[]; + webhooks: CapabilityWebhook[]; + skills: CapabilitySkill[]; + status: string; + last_verified: string; + notes: string[]; +} + +export interface CapabilityBundle { + id: string; + title: string; + intro: string; + url: string; + capabilities: string[]; +} + +export interface CapabilityMap { + $schema: string; + version: number; + generated_at: string; + sources: { + developer_docs: string; + merchant_docs: string; + changelog: string; + admin_api_spec: string; + admin_api_versions: string[]; + stable_api_version: string; + }; + bundles: CapabilityBundle[]; + capabilities: Capability[]; +} + +export const capabilityMap = generated as CapabilityMap; + +export const DEVELOPER_SITE = capabilityMap.sources.developer_docs; +export const MERCHANT_SITE = capabilityMap.sources.merchant_docs; + +const byId = new Map(capabilityMap.capabilities.map((c) => [c.id, c])); + +export function getCapability(id: string): Capability | undefined { + return byId.get(id); +} + +export function getBundle(id: string): CapabilityBundle | undefined { + return capabilityMap.bundles.find((b) => b.id === id); +} + +/** Capabilities whose developer_docs include this site-relative page URL (e.g. /docs/testing). */ +export function capabilitiesForPage(pageUrl: string): Capability[] { + const absolute = `${DEVELOPER_SITE}${pageUrl}`; + return capabilityMap.capabilities.filter((c) => c.developer_docs.includes(absolute)); +} diff --git a/lib/plain-text.ts b/lib/plain-text.ts new file mode 100644 index 00000000..6b815fdf --- /dev/null +++ b/lib/plain-text.ts @@ -0,0 +1,81 @@ +/** + * Turns fumadocs' processed Markdown (which still carries the MDX components the + * site renders) into plain Markdown an agent can read without a JSX parser. + * + * Only the components used under content/docs are handled; the post-build check + * (scripts/check-agent-surfaces.mjs) fails if a bundle still contains a tag or an + * import line, so a new component shows up as a build failure, not as noise. + */ + +function unquote(value: string | undefined): string { + if (!value) return ''; + return value.replace(/^["'{]+|["'}]+$/g, '').trim(); +} + +function attr(tag: string, name: string): string | undefined { + const m = tag.match(new RegExp(`\\b${name}=("[^"]*"|'[^']*'|\\{[^}]*\\})`)); + return m ? unquote(m[1]) : undefined; +} + +const CALLOUT_LABEL: Record = { + info: 'Note', + idea: 'Tip', + warn: 'Warning', + warning: 'Warning', + error: 'Caution', + success: 'Note', +}; + +export function toPlainMarkdown(input: string): string { + let text = input; + + // ESM import/export lines that MDX allows at the top level. + text = text.replace(/^(import|export)\s[^\n]*\n?/gm, ''); + // JSX comments. + text = text.replace(/\{\/\*[\s\S]*?\*\/\}/g, ''); + + // ... -> blockquote with a label. + text = text.replace(/]*)>([\s\S]*?)<\/Callout>/g, (_m, attrs: string, body: string) => { + const type = attr(attrs, 'type') ?? 'info'; + const title = attr(attrs, 'title'); + const label = title ? `${CALLOUT_LABEL[type] ?? 'Note'} (${title})` : CALLOUT_LABEL[type] ?? 'Note'; + const lines = body.trim().split('\n'); + return `> **${label}:** ${lines[0]}${lines.slice(1).map((l) => `\n> ${l}`).join('')}\n`; + }); + + // ... -> bold tab names. + text = text.replace(/]*)>/g, (_m, attrs: string) => { + const value = attr(attrs, 'value') ?? attr(attrs, 'label'); + return value ? `\n**${value}**\n` : '\n'; + }); + text = text.replace(/<\/?Tabs\b[^>]*>/g, '\n'); + text = text.replace(/<\/Tab>/g, '\n'); + + // desc -> link list. + text = text.replace(/]*?)(?:\/>|>([\s\S]*?)<\/Card>)/g, (_m, attrs: string, body?: string) => { + const title = attr(attrs, 'title') ?? 'Link'; + const href = attr(attrs, 'href'); + const desc = (body ?? '').trim().replace(/\s+/g, ' '); + const link = href ? `[${title}](${href})` : title; + return `- ${link}${desc ? `: ${desc}` : ''}\n`; + }); + text = text.replace(/<\/?Cards\b[^>]*>/g, '\n'); + + // -> a mermaid code fence; the diagram source is readable prose. + text = text.replace(/]*?chart=(?:"([\s\S]*?)"|\{`([\s\S]*?)`\})[^>]*\/?>(?:<\/Mermaid>)?/g, (_m, dq?: string, bt?: string) => { + const chart = (dq ?? bt ?? '').trim(); + return chart ? `\n\`\`\`mermaid\n${chart}\n\`\`\`\n` : ''; + }); + + // ... -> the steps' own headings and prose. + text = text.replace(/<\/?Steps?\b[^>]*>/g, '\n'); + + // Any remaining capitalised component (diagrams, playgrounds, grids) carries no + // prose: drop the tag and its children. + text = text.replace(/<([A-Z][A-Za-z0-9]*)\b[^>]*\/>/g, ''); + text = text.replace(/<([A-Z][A-Za-z0-9]*)\b[^>]*>[\s\S]*?<\/\1>/g, ''); + + // Blank-line hygiene. + text = text.replace(/\n{3,}/g, '\n\n'); + return text.trim() + '\n'; +} diff --git a/package.json b/package.json index e3e02b47..c97ea82e 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,20 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "node scripts/generate-api-docs.mjs && node scripts/generate-stats.mjs && npm run generate-graphql-docs && npm run build:preview && next dev --turbopack", - "build": "node scripts/generate-api-docs.mjs && node scripts/generate-stats.mjs && npm run generate-graphql-docs && npm run build:preview && next build", + "dev": "npm run generate && npm run build:preview && next dev --turbopack", + "build": "npm run generate && npm run build:preview && next build", "start": "next start", + "generate": "node scripts/generate-api-docs.mjs && node scripts/generate-stats.mjs && node scripts/generate-graphql-docs.mjs && node scripts/generate-capability-map.mjs", "generate-api-docs": "node scripts/generate-api-docs.mjs", "generate-graphql-docs": "node scripts/generate-graphql-docs.mjs", "generate-stats": "node scripts/generate-stats.mjs", + "generate-capability-map": "node scripts/generate-capability-map.mjs", "build:preview": "node scripts/build-preview.mjs", - "validate-links": "node scripts/validate-links.mjs", + "validate-links": "npm run generate && node scripts/validate-links.mjs", "check-agent-surfaces": "node scripts/check-agent-surfaces.mjs", + "check-frontmatter": "node scripts/check-frontmatter.mjs", + "check": "npm run check-agent-surfaces && npm run check-frontmatter && node scripts/validate-links.mjs", + "check-live-surfaces": "node scripts/check-live-surfaces.mjs", "postinstall": "patch-package" }, "dependencies": { diff --git a/public/capabilities.schema.json b/public/capabilities.schema.json new file mode 100644 index 00000000..ff8fbc21 --- /dev/null +++ b/public/capabilities.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://developers.nextcommerce.com/capabilities.schema.json", + "title": "Next Commerce platform capability map", + "description": "A projection over the Next Commerce documentation sites and API specifications. Each capability connects merchant guides, developer guides, Admin API operations, webhook events, and AI agent skills under one stable id. The owning sources win whenever they disagree with this file.", + "type": "object", + "required": ["version", "generated_at", "sources", "bundles", "capabilities"], + "properties": { + "$schema": { "type": "string" }, + "version": { "const": 1 }, + "generated_at": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, + "sources": { + "type": "object", + "required": ["developer_docs", "merchant_docs", "changelog", "admin_api_spec", "admin_api_versions", "stable_api_version"], + "properties": { + "developer_docs": { "type": "string", "format": "uri" }, + "merchant_docs": { "type": "string", "format": "uri" }, + "changelog": { "type": "string", "format": "uri" }, + "admin_api_spec": { "type": "string", "format": "uri" }, + "admin_api_versions": { "type": "array", "items": { "type": "string" } }, + "stable_api_version": { "type": "string" } + } + }, + "bundles": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "title", "intro", "url", "capabilities"], + "properties": { + "id": { "$ref": "#/$defs/id" }, + "title": { "type": "string" }, + "intro": { "type": "string" }, + "url": { "type": "string", "format": "uri" }, + "capabilities": { "type": "array", "items": { "$ref": "#/$defs/id" } } + } + } + }, + "capabilities": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "title", "summary", "audiences", "operator_docs", "developer_docs", "api_operations", "webhooks", "skills", "status", "last_verified", "notes"], + "properties": { + "id": { "$ref": "#/$defs/id" }, + "title": { "type": "string" }, + "summary": { "type": "string" }, + "audiences": { "type": "array", "items": { "enum": ["merchant", "developer"] } }, + "operator_docs": { "type": "array", "items": { "type": "string", "format": "uri" } }, + "developer_docs": { "type": "array", "items": { "type": "string", "format": "uri" } }, + "api_operations": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "method", "path", "summary", "url"], + "properties": { + "id": { "type": "string" }, + "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] }, + "path": { "type": "string" }, + "summary": { "type": "string" }, + "url": { "type": ["string", "null"] } + } + } + }, + "webhooks": { + "type": "array", + "items": { + "type": "object", + "required": ["event", "url"], + "properties": { + "event": { "type": "string", "pattern": "^[a-z_]+\\.[a-z_]+$" }, + "url": { "type": ["string", "null"] } + } + } + }, + "skills": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "url"], + "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri" } } + } + }, + "status": { "enum": ["available", "beta", "deprecated"] }, + "last_verified": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, + "notes": { "type": "array", "items": { "type": "string" } } + } + } + } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" } + } +} diff --git a/scripts/generate-capability-map.mjs b/scripts/generate-capability-map.mjs new file mode 100644 index 00000000..0ec3842e --- /dev/null +++ b/scripts/generate-capability-map.mjs @@ -0,0 +1,376 @@ +/** + * Generates lib/generated/capabilities.json, the platform capability map served at + * /capabilities.json and consumed by the /llms/.txt routes, the capability + * page, and the merchant docs site. + * + * Source: content/capabilities.yaml (hand-authored index; see its header comment). + * Run after generate-api-docs.mjs, because developer page existence is checked + * against content/docs including the generated reference pages. + * + * The map is a projection over owning sources, so everything that can be derived + * is derived and validated here rather than typed twice: + * - api_operations "tag:" selectors expand to the operations carrying that + * tag in the stable spec; bare entries must be operationIds in the same spec + * - webhooks must be events in the spec's `webhooks` block ("all" expands to every event) + * - developer_docs must resolve to a page under content/docs + * - skills must appear in the AI Skills table + * - operator_docs are merchant-site paths; they are shape-checked here and verified + * live by scripts/check-live-surfaces.mjs and by the merchant repo's own CI + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'; +import { join, resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { load as loadYaml } from 'js-yaml'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, '..'); + +const SOURCE_PATH = join(ROOT, 'content', 'capabilities.yaml'); +const SKILLS_PAGE = join(ROOT, 'content', 'docs', 'skills', 'index.mdx'); +const OUT_DIR = join(ROOT, 'lib', 'generated'); +const OUT_PATH = join(OUT_DIR, 'capabilities.json'); + +export const DEVELOPER_SITE = 'https://developers.nextcommerce.com'; +export const MERCHANT_SITE = 'https://docs.nextcommerce.com'; + +const STATUSES = ['available', 'beta', 'deprecated']; +const AUDIENCES = ['merchant', 'developer']; +const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete']; +const ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +const errors = []; +function fail(message) { + errors.push(message); +} + +const map = loadYaml(readFileSync(SOURCE_PATH, 'utf8')); +if (!map || map.version !== 1) fail('capabilities.yaml: expected `version: 1`'); +const specVersion = String(map.spec_version ?? ''); +const specPath = join(ROOT, 'public', 'api', 'admin', `${specVersion}.yaml`); +if (!existsSync(specPath)) fail(`capabilities.yaml: spec_version ${specVersion} has no file at public/api/admin/`); +const spec = existsSync(specPath) ? loadYaml(readFileSync(specPath, 'utf8')) : { paths: {}, webhooks: {} }; + +// ---- owning sources ------------------------------------------------------- + +const operationsById = new Map(); +const operationsByTag = new Map(); +for (const [path, item] of Object.entries(spec.paths ?? {})) { + if (!item || typeof item !== 'object') continue; + for (const method of HTTP_METHODS) { + const op = item[method]; + if (!op) continue; + const tags = Array.isArray(op.tags) ? op.tags : []; + const record = { + id: op.operationId, + method: method.toUpperCase(), + path, + // The spec carries prose in `description`; keep the first sentence as the summary. + summary: String(op.summary ?? op.description ?? '').split(/(?<=\.)\s/)[0].trim(), + tag: tags[0] ?? null, + }; + if (!record.id) { + fail(`spec: ${method.toUpperCase()} ${path} has no operationId`); + continue; + } + operationsById.set(record.id, record); + for (const tag of tags) { + const list = operationsByTag.get(tag) ?? []; + list.push(record); + operationsByTag.set(tag, list); + } + } +} +const allEvents = Object.keys(spec.webhooks ?? {}).sort(); + +const skillsTable = readFileSync(SKILLS_PAGE, 'utf8'); +const knownSkills = new Set( + [...skillsTable.matchAll(/^\|\s*\[\*\*([a-z0-9-]+)\*\*\]/gm)].map((m) => m[1]), +); +if (knownSkills.size === 0) fail('skills page: no skill rows found in the table'); + +// A developer path exists when content/docs holds .mdx, .md, or /index.mdx. +function developerPageExists(path) { + const rel = path.replace(/^\/docs\/?/, ''); + const base = join(ROOT, 'content', 'docs', rel); + return ( + existsSync(`${base}.mdx`) || + existsSync(`${base}.md`) || + existsSync(join(base, 'index.mdx')) || + existsSync(join(base, 'index.md')) + ); +} + +// Reference page for an operation, as generate-api-docs lays it out. +function operationUrl(op) { + const dir = join(ROOT, 'content', 'docs', 'admin-api', 'reference', op.tag ?? '', `${op.id}.mdx`); + return existsSync(dir) ? `/docs/admin-api/reference/${op.tag}/${op.id}` : null; +} + +// Webhook reference pages are grouped by tag folder: /docs/webhooks/reference//. +const webhookRefDir = join(ROOT, 'content', 'docs', 'webhooks', 'reference'); +const webhookPages = new Map(); +if (existsSync(webhookRefDir)) { + for (const entry of readdirSync(webhookRefDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + for (const file of readdirSync(join(webhookRefDir, entry.name))) { + if (file.endsWith('.mdx')) webhookPages.set(file.replace(/\.mdx$/, ''), `/docs/webhooks/reference/${entry.name}/${file.replace(/\.mdx$/, '')}`); + } + } +} +function webhookUrl(event) { + return webhookPages.get(event) ?? null; +} + +// ---- validate and resolve records ---------------------------------------- + +function expectArray(value, label) { + if (value === undefined || value === null) return []; + if (!Array.isArray(value)) { + fail(`${label}: expected a list`); + return []; + } + return value; +} + +const ids = new Set(); +const capabilities = []; +for (const raw of expectArray(map.capabilities, 'capabilities')) { + const label = `capability ${raw?.id ?? '(no id)'}`; + if (!raw || typeof raw !== 'object') { + fail(`${label}: not a mapping`); + continue; + } + if (!ID_RE.test(String(raw.id))) fail(`${label}: id must be kebab-case`); + if (ids.has(raw.id)) fail(`${label}: duplicate id`); + ids.add(raw.id); + if (!raw.title) fail(`${label}: missing title`); + if (!raw.summary || String(raw.summary).trim().length < 40) fail(`${label}: summary must be real prose`); + if (!STATUSES.includes(raw.status)) fail(`${label}: status must be one of ${STATUSES.join(', ')}`); + if (!DATE_RE.test(String(raw.last_verified))) fail(`${label}: last_verified must be YYYY-MM-DD`); + + const audiences = expectArray(raw.audiences, `${label}.audiences`); + if (audiences.length === 0) fail(`${label}: at least one audience`); + for (const a of audiences) if (!AUDIENCES.includes(a)) fail(`${label}: unknown audience ${a}`); + // Every audience a record claims must be backed by evidence on that audience's site. + if (audiences.includes('merchant') && expectArray(raw.operator_docs, `${label}.operator_docs`).length === 0) + fail(`${label}: audience merchant needs at least one operator_docs page`); + if (audiences.includes('developer') && expectArray(raw.developer_docs, `${label}.developer_docs`).length === 0) + fail(`${label}: audience developer needs at least one developer_docs page`); + + const operatorDocs = expectArray(raw.operator_docs, `${label}.operator_docs`); + for (const p of operatorDocs) { + if (!/^\/(docs\/[a-z0-9\-/]+|changelog)$/.test(p)) fail(`${label}: operator path ${p} is not a site-relative merchant path`); + } + + const developerDocs = expectArray(raw.developer_docs, `${label}.developer_docs`); + for (const p of developerDocs) { + if (!p.startsWith('/docs')) fail(`${label}: developer path ${p} must start with /docs`); + else if (!developerPageExists(p)) fail(`${label}: developer page ${p} does not exist under content/docs`); + } + + const operations = []; + const seenOps = new Set(); + for (const selector of expectArray(raw.api_operations, `${label}.api_operations`)) { + const s = String(selector); + let matched = []; + if (s.startsWith('tag:')) { + matched = operationsByTag.get(s.slice(4)) ?? []; + if (matched.length === 0) fail(`${label}: no operations carry tag ${s.slice(4)} in the ${specVersion} spec`); + } else if (operationsById.has(s)) { + matched = [operationsById.get(s)]; + } else { + fail(`${label}: unknown operationId ${s} in the ${specVersion} spec`); + } + for (const op of matched) { + if (seenOps.has(op.id)) continue; + seenOps.add(op.id); + operations.push({ + id: op.id, + method: op.method, + path: op.path, + summary: op.summary, + url: operationUrl(op), + }); + } + } + + let webhooks = raw.webhooks === 'all' ? allEvents : expectArray(raw.webhooks, `${label}.webhooks`); + for (const e of webhooks) { + if (!allEvents.includes(e)) fail(`${label}: unknown webhook event ${e} in the ${specVersion} spec`); + } + + const skills = expectArray(raw.skills, `${label}.skills`); + for (const s of skills) if (!knownSkills.has(s)) fail(`${label}: unknown skill ${s} (not on the AI Skills page)`); + + const notes = expectArray(raw.notes, `${label}.notes`).map(String); + + capabilities.push({ + id: raw.id, + title: raw.title, + summary: String(raw.summary).replace(/\s+/g, ' ').trim(), + audiences, + operator_docs: operatorDocs.map((p) => `${MERCHANT_SITE}${p}`), + developer_docs: developerDocs.map((p) => `${DEVELOPER_SITE}${p}`), + api_operations: operations.map((op) => ({ + ...op, + url: op.url ? `${DEVELOPER_SITE}${op.url}` : null, + })), + webhooks: webhooks.map((event) => ({ + event, + url: webhookUrl(event) ? `${DEVELOPER_SITE}${webhookUrl(event)}` : null, + })), + skills: skills.map((name) => ({ + name, + url: `https://github.com/NextCommerceCo/skills/tree/main/${name}`, + })), + status: raw.status, + last_verified: String(raw.last_verified), + notes, + }); +} + +const bundleIds = new Set(); +const bundles = []; +for (const raw of expectArray(map.bundles, 'bundles')) { + const label = `bundle ${raw?.id ?? '(no id)'}`; + if (!ID_RE.test(String(raw?.id))) fail(`${label}: id must be kebab-case`); + if (bundleIds.has(raw.id)) fail(`${label}: duplicate id`); + bundleIds.add(raw.id); + if (!raw.title) fail(`${label}: missing title`); + if (!raw.intro || String(raw.intro).trim().length < 40) fail(`${label}: intro must be real prose`); + const members = expectArray(raw.capabilities, `${label}.capabilities`); + if (members.length === 0) fail(`${label}: lists no capabilities`); + for (const id of members) if (!ids.has(id)) fail(`${label}: unknown capability ${id}`); + bundles.push({ + id: raw.id, + title: raw.title, + intro: String(raw.intro).replace(/\s+/g, ' ').trim(), + url: `${DEVELOPER_SITE}/llms/${raw.id}.txt`, + capabilities: members, + }); +} +for (const id of ids) { + if (!bundles.some((b) => b.capabilities.includes(id))) fail(`capability ${id} belongs to no bundle`); +} + +if (errors.length > 0) { + console.error('generate-capability-map: FAIL'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +const specVersions = readdirSync(join(ROOT, 'public', 'api', 'admin')) + .filter((f) => f.endsWith('.yaml')) + .map((f) => f.replace(/\.yaml$/, '')) + .sort(); + +const output = { + $schema: `${DEVELOPER_SITE}/capabilities.schema.json`, + version: 1, + generated_at: new Date().toISOString().slice(0, 10), + sources: { + developer_docs: DEVELOPER_SITE, + merchant_docs: MERCHANT_SITE, + changelog: `${MERCHANT_SITE}/changelog`, + admin_api_spec: `${DEVELOPER_SITE}/api/admin/${specVersion}.yaml`, + admin_api_versions: specVersions, + stable_api_version: specVersion, + }, + bundles, + capabilities, +}; + +mkdirSync(OUT_DIR, { recursive: true }); +writeFileSync(OUT_PATH, JSON.stringify(output, null, 2) + '\n'); +console.log( + `Generated ${OUT_PATH}: ${capabilities.length} capabilities, ${bundles.length} bundles, ` + + `${capabilities.reduce((n, c) => n + c.api_operations.length, 0)} operation links, ` + + `${capabilities.reduce((n, c) => n + c.webhooks.length, 0)} webhook links`, +); + +// ---- readable page ---------------------------------------------------------- +// content/docs/capabilities/index.mdx is generated (git-ignored) so the page and +// the JSON can never list different ids or links. meta.json beside it is committed. + +function mdxEscape(text) { + return text.replace(/[{}<>]/g, (ch) => ({ '{': '{', '}': '}', '<': '<', '>': '>' })[ch]); +} + +// Links to /docs pages stay site-relative so validate-links checks them; links to +// non-docs routes (JSON, specs, bundles) stay absolute, which the link validator +// cannot see and which agents copy verbatim. +function relative(url) { + return url.startsWith(`${DEVELOPER_SITE}/docs/`) ? url.slice(DEVELOPER_SITE.length) : url; +} + +const page = []; +page.push('---'); +page.push('title: Platform Capabilities'); +page.push('description: One record per platform capability linking merchant guides, developer guides, Admin API operations, webhook events, and AI agent skills under a stable id'); +page.push('full: true'); +page.push('---'); +page.push(''); +page.push(`This page and [capabilities.json](${DEVELOPER_SITE}/capabilities.json) are the same generated projection (${output.generated_at}) over the two documentation sites and the Admin API specification. Each capability has a stable id that pages on both sites declare in their frontmatter. Links, operations, and events come from the owning sources; when they disagree with this page, the guide, the [spec](${DEVELOPER_SITE}/api/admin/${specVersion}.yaml), or the [changelog](${MERCHANT_SITE}/changelog) wins.`); +page.push(''); +page.push(`Agents: fetch a [domain bundle](${DEVELOPER_SITE}/llms.txt) rather than this page when you need the prose behind a capability. The JSON schema is at [capabilities.schema.json](${DEVELOPER_SITE}/capabilities.schema.json).`); +page.push(''); +page.push('## Bundles'); +page.push(''); +page.push('| Bundle | Capabilities | Plain-text URL |'); +page.push('| --- | --- | --- |'); +for (const b of bundles) { + page.push(`| ${b.title} | ${b.capabilities.map((id) => `[${id}](#${id})`).join(', ')} | [${b.url.replace(DEVELOPER_SITE, '')}](${b.url}) |`); +} +page.push(''); +page.push('## Capabilities'); +page.push(''); +for (const c of capabilities) { + page.push(`### ${mdxEscape(c.title)} [#${c.id}]`); + page.push(''); + page.push(`\`id: ${c.id}\` · status: ${c.status} · audiences: ${c.audiences.join(', ')} · links verified ${c.last_verified}`); + page.push(''); + page.push(mdxEscape(c.summary)); + page.push(''); + for (const n of c.notes) page.push(`> ${mdxEscape(n)}`); + if (c.notes.length > 0) page.push(''); + if (c.operator_docs.length > 0) { + page.push('**Merchant guides**'); + page.push(''); + for (const u of c.operator_docs) page.push(`- [${u.replace(MERCHANT_SITE, 'docs.nextcommerce.com')}](${u})`); + page.push(''); + } + if (c.developer_docs.length > 0) { + page.push('**Developer guides**'); + page.push(''); + for (const u of c.developer_docs) page.push(`- [${relative(u)}](${relative(u)})`); + page.push(''); + } + if (c.api_operations.length > 0) { + page.push(`**Admin API operations (${c.api_operations.length}, version ${specVersion})**`); + page.push(''); + for (const op of c.api_operations) { + const text = `\`${op.method} ${op.path}\`${op.summary ? ` ${mdxEscape(op.summary)}` : ''}`; + page.push(op.url ? `- [${text}](${relative(op.url)})` : `- ${text}`); + } + page.push(''); + } + if (c.webhooks.length > 0) { + page.push(`**Webhook events (${c.webhooks.length})**`); + page.push(''); + for (const w of c.webhooks) page.push(w.url ? `- [\`${w.event}\`](${relative(w.url)})` : `- \`${w.event}\``); + page.push(''); + } + if (c.skills.length > 0) { + page.push('**AI agent skills**'); + page.push(''); + for (const s of c.skills) page.push(`- [${s.name}](${s.url})`); + page.push(''); + } +} + +const pageDir = join(ROOT, 'content', 'docs', 'capabilities'); +mkdirSync(pageDir, { recursive: true }); +writeFileSync(join(pageDir, 'index.mdx'), page.join('\n') + '\n'); +console.log(`Generated ${join(pageDir, 'index.mdx')}`); diff --git a/source.config.ts b/source.config.ts index 3d0c5555..27010ea8 100644 --- a/source.config.ts +++ b/source.config.ts @@ -69,6 +69,16 @@ export const docs = defineDocs({ schema: frontmatterSchema.extend({ title: z.string().optional().default(''), full: z.boolean().optional(), + // Agent-retrieval metadata. `description` (from frontmatterSchema) is required + // on every authored page by scripts/check-frontmatter.mjs; the fields below are + // optional and validated against the capability map by the same script. + audience: z.array(z.enum(['merchant', 'developer'])).optional(), + capability_ids: z.array(z.string()).optional(), + status: z.enum(['available', 'beta', 'deprecated']).optional(), + last_verified: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/, 'last_verified must be YYYY-MM-DD') + .optional(), }), postprocess: { includeProcessedMarkdown: true, From a7ace1564300d23585619cc4502b2c7e4a0cd3fb Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Thu, 3 Sep 2026 16:29:38 +0700 Subject: [PATCH 2/7] Deterministic validation: regenerate before validate-links, all heading fragments, first CI workflow validate-links now regenerates the reference trees first (same sequence locally and in CI) and accepts fragments for every heading level, which clears the standing h4 false alarm in apps/guides/fulfillment-service.mdx:150. check-agent-surfaces gains assertions for the capability map, the readable page, the bundles (size budget 400 KB, no MDX residue, absolute links), and llms.txt ordering. .github/workflows/ci.yml: npm ci, build, check. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 31 +++++++++++++ scripts/check-agent-surfaces.mjs | 77 ++++++++++++++++++++++++++++++++ scripts/validate-links.mjs | 44 +++++++++++++++++- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ba0e46e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +# The same sequence a clean checkout runs locally: +# npm ci -> npm run build (generates references, stats, capability map, previews, +# then next build) -> npm run check (post-build agent-surface assertions, +# frontmatter, link validation against the generated tree). +# `validate-links` alone regenerates first; here the build already did. + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: '22' + cache: npm + + - run: npm ci + + - run: npm run build + + - run: npm run check diff --git a/scripts/check-agent-surfaces.mjs b/scripts/check-agent-surfaces.mjs index c52c3df6..44f55c87 100644 --- a/scripts/check-agent-surfaces.mjs +++ b/scripts/check-agent-surfaces.mjs @@ -69,6 +69,83 @@ if (statsRaw !== null) { } } +// Capability map: the JSON served at /capabilities.json, its schema, and the +// readable page must exist and agree on ids. +const capPath = join(OUT, 'capabilities.json'); +const capRaw = read(capPath); +check(capRaw !== null, 'out/capabilities.json does not exist'); +let capabilityMap = null; +if (capRaw !== null) { + try { + capabilityMap = JSON.parse(capRaw); + } catch (e) { + check(false, `capabilities.json is not valid JSON: ${e.message}`); + } +} +check(read(join(OUT, 'capabilities.schema.json')) !== null, 'out/capabilities.schema.json does not exist'); +if (capabilityMap) { + check(capabilityMap.version === 1, `capabilities.json version is ${capabilityMap.version}`); + check(Array.isArray(capabilityMap.capabilities) && capabilityMap.capabilities.length >= 8, 'capabilities.json has fewer than 8 capabilities'); + check(Array.isArray(capabilityMap.bundles) && capabilityMap.bundles.length === 6, 'capabilities.json does not list 6 bundles'); + const capPage = read(join(OUT, 'docs', 'capabilities.html')) ?? read(join(OUT, 'docs', 'capabilities', 'index.html')); + check(capPage !== null, 'out/docs/capabilities.html does not exist'); + if (capPage !== null) { + for (const c of capabilityMap.capabilities) { + check(capPage.includes(`id="${c.id}"`), `capability page has no anchor for ${c.id}`); + for (const url of c.operator_docs) check(capPage.includes(url), `capability page is missing merchant link ${url}`); + } + } + for (const c of capabilityMap.capabilities) { + for (const url of c.developer_docs) { + const rel = url.replace('https://developers.nextcommerce.com', ''); + const html = read(join(OUT, `${rel}.html`)) ?? read(join(OUT, rel, 'index.html')); + check(html !== null, `developer page ${rel} cited by ${c.id} was not built`); + // The reciprocal panel must appear on every cited developer page. + if (html !== null && c.operator_docs.length > 0) { + check(html.includes('Related merchant guides'), `developer page ${rel} has no reciprocal merchant-guide panel`); + } + } + } +} + +// Domain bundles: plain Markdown, absolute links, bounded size, no MDX residue. +// Budget: 400 KB per bundle. The largest (storefront, 18 pages) is ~200 KB at +// 2026-09-03; the full corpus is ~1.5 MB. Raise the budget deliberately, in this +// file, if a bundle legitimately grows past it. +const BUNDLE_BUDGET_BYTES = 400_000; +const MDX_RESIDUE = /^(import|export)\s|<\/?[A-Z][A-Za-z0-9]*[\s>/]|\{\/\*/m; +// Placeholders like inside code fences are prose, not components. +function outsideCodeFences(text) { + return text.replace(/^(```|~~~)[\s\S]*?^\1[^\n]*$/gm, ''); +} +if (capabilityMap) { + for (const b of capabilityMap.bundles) { + const path = join(OUT, 'llms', `${b.id}.txt`); + const text = read(path); + check(text !== null, `out/llms/${b.id}.txt does not exist`); + if (text === null) continue; + const bytes = Buffer.byteLength(text, 'utf8'); + check(bytes <= BUNDLE_BUDGET_BYTES, `llms/${b.id}.txt is ${bytes} bytes, over the ${BUNDLE_BUDGET_BYTES} byte budget`); + check(text.startsWith(`# Next Commerce: ${b.title}`), `llms/${b.id}.txt first line is not the bundle title`); + check(!MDX_RESIDUE.test(outsideCodeFences(text)), `llms/${b.id}.txt still contains MDX (import/export line or a component tag)`); + const relative = text.match(/\]\(\//g) ?? []; + check(relative.length === 0, `llms/${b.id}.txt has ${relative.length} relative markdown link(s)`); + check(text.includes('## Pages'), `llms/${b.id}.txt has no Pages section`); + for (const id of b.capabilities) check(text.includes(`(id: ${id})`), `llms/${b.id}.txt does not render capability ${id}`); + } + // llms.txt must advertise every bundle and the map, and demote the full corpus. + if (llms !== null) { + check(llms.includes('/capabilities.json'), 'llms.txt does not link the capability map'); + for (const b of capabilityMap.bundles) check(llms.includes(b.url), `llms.txt does not link bundle ${b.id}`); + const bundlesAt = llms.indexOf('## Domain bundles'); + const fullAt = llms.indexOf('/llms-full.txt'); + check(bundlesAt !== -1 && fullAt > bundlesAt, 'llms.txt lists the full corpus before the domain bundles'); + } +} + +// 404 page links the capability map. +if (notFound !== null) check(notFound.includes('/docs/capabilities'), '404.html does not link the capability map'); + if (failures.length > 0) { console.error('check-agent-surfaces: FAIL'); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/validate-links.mjs b/scripts/validate-links.mjs index 9cce3f9a..ebf71df1 100644 --- a/scripts/validate-links.mjs +++ b/scripts/validate-links.mjs @@ -1,8 +1,45 @@ import path from 'node:path'; +import GithubSlugger from 'github-slugger'; import { getTableOfContents } from 'fumadocs-core/content/toc'; import { getSlugs } from 'fumadocs-core/source'; import { printErrors, readFiles, scanURLs, validateFiles } from 'next-validate-link'; +/** + * Validates every internal link under content/docs, including fragments. + * + * Run after the reference generators (`npm run generate`); the generated + * Admin API, webhook, GraphQL, and Campaigns API pages are link targets, and + * without them this reports dozens of false "missing page" errors. The + * `validate-links` npm script regenerates first so the sequence is the same + * locally and in CI. + * + * Fragments: rehype-slug gives every rendered heading (h1 to h6) an id, but the + * table of contents only records the levels it shows, so a link to an h4 used to + * fail as `invalid-fragment` (content/docs/apps/guides/fulfillment-service.mdx:150 + * was the standing example). The valid fragments for a page are therefore the + * union of the TOC entries and every heading in the source, slugged the same way. + */ + +function headingHashes(content) { + const slugger = new GithubSlugger(); + const hashes = []; + let inFence = false; + for (const line of content.split('\n')) { + if (/^\s*(```|~~~)/.test(line)) inFence = !inFence; + if (inFence) continue; + const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/); + if (!m) continue; + const custom = m[1].match(/\[#([^\]]+)\]\s*$/); + if (custom) { + hashes.push(custom[1]); + continue; + } + const text = m[1].replace(/`([^`]*)`/g, '$1').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').trim(); + hashes.push(slugger.slug(text)); + } + return hashes; +} + async function checkLinks() { const docsFiles = await readFiles('content/docs/**/*.{md,mdx}'); @@ -10,7 +47,12 @@ async function checkLinks() { populate: { 'docs/[[...slug]]': docsFiles.map((file) => ({ value: getSlugs(path.relative('content/docs', file.path)), - hashes: getTableOfContents(file.content).map((item) => item.url.slice(1)), + hashes: [ + ...new Set([ + ...getTableOfContents(file.content).map((item) => item.url.slice(1)), + ...headingHashes(file.content), + ]), + ], })), }, }); From d0970bb33c2134c8d0c29ef16ba96a2ca216f2d4 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Thu, 3 Sep 2026 16:29:38 +0700 Subject: [PATCH 3/7] Page metadata: descriptions on 60 authored pages, capability_ids derived from the map Counting rule (frozen): git-tracked content/docs/**/*.{md,mdx}; generated trees are ignored. 67 authored pages, 60 lacked a description. check-frontmatter.mjs requires a description on every authored page, validates the new fields, and keeps capability_ids consistent with the map (--write inserts them). Also fixes two typos found in passing (nak push, Submitting). Co-Authored-By: Claude Fable 5.1 --- content/docs/admin-api/guides/exports.mdx | 2 + .../admin-api/guides/external-checkout.mdx | 2 + .../admin-api/guides/order-management.mdx | 2 + .../guides/payment-methods/affirm.mdx | 2 + .../guides/payment-methods/afterpay.mdx | 2 + .../guides/payment-methods/apple-pay.mdx | 2 + .../guides/payment-methods/bancontact.mdx | 2 + .../guides/payment-methods/bankcard.mdx | 2 + .../guides/payment-methods/google-pay.mdx | 2 + .../guides/payment-methods/ideal.mdx | 2 + .../guides/payment-methods/index.mdx | 2 + .../guides/payment-methods/klarna.mdx | 2 + .../admin-api/guides/payment-methods/link.mdx | 2 + .../guides/payment-methods/paypal.mdx | 2 + .../guides/payment-methods/sepa-debit.mdx | 2 + .../guides/payment-methods/swish.mdx | 2 + .../guides/payment-methods/twint.mdx | 2 + .../guides/subscription-management.mdx | 2 + .../docs/admin-api/guides/testing-guide.mdx | 2 + content/docs/admin-api/index.mdx | 2 + content/docs/admin-api/permissions.md | 2 + content/docs/apps/app-development-flow.mdx | 2 + content/docs/apps/app-kit.mdx | 2 + content/docs/apps/assets.mdx | 1 + content/docs/apps/event-tracking.mdx | 2 + content/docs/apps/guides/dispute-service.mdx | 2 + .../docs/apps/guides/fulfillment-service.mdx | 2 + .../apps/guides/marketing-attribution.mdx | 2 + .../apps/guides/server-to-server-apps.mdx | 2 + .../docs/apps/guides/storefront-extension.mdx | 4 +- content/docs/apps/index.mdx | 2 + content/docs/apps/manifest.mdx | 2 + content/docs/apps/oauth/getting-started.mdx | 2 + content/docs/apps/oauth/index.mdx | 2 + content/docs/apps/oauth/install-flows.md | 2 + content/docs/apps/oauth/session-auth.mdx | 2 + content/docs/apps/review.mdx | 4 +- content/docs/apps/settings.mdx | 2 + content/docs/apps/snippets.mdx | 2 + content/docs/campaigns/admin-api/index.mdx | 1 + content/docs/campaigns/api/index.mdx | 2 + content/docs/campaigns/index.mdx | 2 + content/docs/campaigns/page-kit.mdx | 1 + content/docs/campaigns/templates.mdx | 1 + content/docs/index.mdx | 1 + content/docs/skills/index.mdx | 1 + content/docs/storefront/checkout-links.mdx | 2 + content/docs/storefront/event-tracking.mdx | 2 + content/docs/storefront/graphql/index.mdx | 2 + content/docs/storefront/index.md | 2 + .../storefront/themes/cdn-and-caching.mdx | 2 + .../themes/guides/custom-page-templates.mdx | 2 + .../guides/custom-product-templates.mdx | 2 + .../themes/guides/personalized-products.mdx | 2 + .../themes/guides/product-metadata.md | 2 + .../themes/guides/product-variants.mdx | 2 + content/docs/storefront/themes/index.mdx | 2 + content/docs/storefront/themes/settings.mdx | 2 + .../storefront/themes/templates/filters.md | 2 + .../storefront/themes/templates/index.mdx | 2 + .../storefront/themes/templates/objects.mdx | 2 + .../docs/storefront/themes/templates/tags.mdx | 2 + .../templates/urls-and-template-paths.mdx | 2 + content/docs/storefront/themes/theme-kit.mdx | 2 + .../docs/storefront/themes/translations.mdx | 2 + content/docs/testing/index.mdx | 1 + content/docs/webhooks/index.mdx | 1 + scripts/check-frontmatter.mjs | 140 ++++++++++++++++++ 68 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 scripts/check-frontmatter.mjs diff --git a/content/docs/admin-api/guides/exports.mdx b/content/docs/admin-api/guides/exports.mdx index feaf19bf..fcd884a9 100644 --- a/content/docs/admin-api/guides/exports.mdx +++ b/content/docs/admin-api/guides/exports.mdx @@ -1,5 +1,7 @@ --- title: API Exports +description: Generate, poll, and download CSV data exports through the Admin API, with available export types and the export.created webhook +capability_ids: [admin-api] sidebar_label: Exports sidebar_position: 3 tags: diff --git a/content/docs/admin-api/guides/external-checkout.mdx b/content/docs/admin-api/guides/external-checkout.mdx index c63d1f85..bd72bafa 100644 --- a/content/docs/admin-api/guides/external-checkout.mdx +++ b/content/docs/admin-api/guides/external-checkout.mdx @@ -1,5 +1,7 @@ --- title: External Checkout Flow +description: Create carts and orders from an external checkout through the Admin API, including upsells, line items, and payment details +capability_ids: [orders] sidebar_label: External Checkout Flow sidebar_position: 1 tags: diff --git a/content/docs/admin-api/guides/order-management.mdx b/content/docs/admin-api/guides/order-management.mdx index 24c0d9d4..185cad48 100644 --- a/content/docs/admin-api/guides/order-management.mdx +++ b/content/docs/admin-api/guides/order-management.mdx @@ -1,5 +1,7 @@ --- title: API Order Management +description: Edit order line items, refund, update shipping addresses, manage fulfillment orders, add tracking, and cancel orders through the Admin API +capability_ids: [orders] sidebar_label: Order Management sidebar_position: 1 tags: diff --git a/content/docs/admin-api/guides/payment-methods/affirm.mdx b/content/docs/admin-api/guides/payment-methods/affirm.mdx index 206ad352..da6d7872 100644 --- a/content/docs/admin-api/guides/payment-methods/affirm.mdx +++ b/content/docs/admin-api/guides/payment-methods/affirm.mdx @@ -1,5 +1,7 @@ --- title: Affirm Admin API Guide +description: Create Admin API orders paid with Affirm using the redirect payment flow, payment_return_url, and payment_complete_url +capability_ids: [payments-gateways] sidebar_label: Affirm sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/payment-methods/afterpay.mdx b/content/docs/admin-api/guides/payment-methods/afterpay.mdx index 62744bb7..79a7ae95 100644 --- a/content/docs/admin-api/guides/payment-methods/afterpay.mdx +++ b/content/docs/admin-api/guides/payment-methods/afterpay.mdx @@ -1,5 +1,7 @@ --- title: Afterpay Admin API Guide +description: Create Admin API orders paid with Afterpay (Clearpay in the UK) using the redirect payment flow, with upsell and subscription limitations +capability_ids: [payments-gateways] sidebar_label: Afterpay tags: - Guide diff --git a/content/docs/admin-api/guides/payment-methods/apple-pay.mdx b/content/docs/admin-api/guides/payment-methods/apple-pay.mdx index 93f5e3d1..5f86abc1 100644 --- a/content/docs/admin-api/guides/payment-methods/apple-pay.mdx +++ b/content/docs/admin-api/guides/payment-methods/apple-pay.mdx @@ -1,5 +1,7 @@ --- title: Apple Pay Admin API Guide +description: Create Admin API orders paid with Apple Pay using the redirect payment flow, including standard and one-click checkout options +capability_ids: [payments-gateways] sidebar_label: Apple Pay sidebar_position: 3 tags: diff --git a/content/docs/admin-api/guides/payment-methods/bancontact.mdx b/content/docs/admin-api/guides/payment-methods/bancontact.mdx index 982e9726..f1cb3bd1 100644 --- a/content/docs/admin-api/guides/payment-methods/bancontact.mdx +++ b/content/docs/admin-api/guides/payment-methods/bancontact.mdx @@ -1,5 +1,7 @@ --- title: Bancontact Admin API Guide +description: Create Admin API orders paid with Bancontact using the redirect payment flow, payment_return_url, and payment_complete_url +capability_ids: [payments-gateways] sidebar_label: Bancontact sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/payment-methods/bankcard.mdx b/content/docs/admin-api/guides/payment-methods/bankcard.mdx index da9e3516..ce920c08 100644 --- a/content/docs/admin-api/guides/payment-methods/bankcard.mdx +++ b/content/docs/admin-api/guides/payment-methods/bankcard.mdx @@ -1,5 +1,7 @@ --- title: Bankcard +description: Charge tokenized bankcards on the Admin API with card_token, including gateway routing, iFrame card tokenization, and 3D Secure (3DS2) +capability_ids: [payments-gateways] sidebar_label: Bankcard sidebar_position: 2 tags: diff --git a/content/docs/admin-api/guides/payment-methods/google-pay.mdx b/content/docs/admin-api/guides/payment-methods/google-pay.mdx index 7718023c..a0a42ebb 100644 --- a/content/docs/admin-api/guides/payment-methods/google-pay.mdx +++ b/content/docs/admin-api/guides/payment-methods/google-pay.mdx @@ -1,5 +1,7 @@ --- title: Google Pay Admin API Guide +description: Create Admin API orders paid with Google Pay using the redirect payment flow, including standard and one-click checkout options +capability_ids: [payments-gateways] sidebar_label: Google Pay sidebar_position: 3 tags: diff --git a/content/docs/admin-api/guides/payment-methods/ideal.mdx b/content/docs/admin-api/guides/payment-methods/ideal.mdx index 42507014..8b7e4384 100644 --- a/content/docs/admin-api/guides/payment-methods/ideal.mdx +++ b/content/docs/admin-api/guides/payment-methods/ideal.mdx @@ -1,5 +1,7 @@ --- title: iDEAL Admin API Guide +description: Create Admin API orders paid with iDEAL using the redirect payment flow, payment_return_url, and payment_complete_url +capability_ids: [payments-gateways] sidebar_label: iDEAL sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/payment-methods/index.mdx b/content/docs/admin-api/guides/payment-methods/index.mdx index e1d58351..e640da49 100644 --- a/content/docs/admin-api/guides/payment-methods/index.mdx +++ b/content/docs/admin-api/guides/payment-methods/index.mdx @@ -1,5 +1,7 @@ --- title: Payment Methods +description: Capability matrix of Admin API payment methods by flow type, express checkout, upsell, and subscription support, with links to each guide +capability_ids: [payments-gateways] sidebar_label: Payment Methods tags: - Guide diff --git a/content/docs/admin-api/guides/payment-methods/klarna.mdx b/content/docs/admin-api/guides/payment-methods/klarna.mdx index 75a37150..b04e079b 100644 --- a/content/docs/admin-api/guides/payment-methods/klarna.mdx +++ b/content/docs/admin-api/guides/payment-methods/klarna.mdx @@ -1,5 +1,7 @@ --- title: Klarna Admin API Guide +description: Create Admin API orders paid with Klarna using the redirect payment flow, with support for one-click upsells and subscription items +capability_ids: [payments-gateways] sidebar_label: Klarna sidebar_position: 4 tags: diff --git a/content/docs/admin-api/guides/payment-methods/link.mdx b/content/docs/admin-api/guides/payment-methods/link.mdx index e0276f45..21deff87 100644 --- a/content/docs/admin-api/guides/payment-methods/link.mdx +++ b/content/docs/admin-api/guides/payment-methods/link.mdx @@ -1,5 +1,7 @@ --- title: Link Admin API Guide +description: Create Admin API orders paid with Link via Stripe using the redirect payment flow, with support for one-click upsells and subscription items +capability_ids: [payments-gateways] sidebar_label: Link sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/payment-methods/paypal.mdx b/content/docs/admin-api/guides/payment-methods/paypal.mdx index 60b9b890..aa73533a 100644 --- a/content/docs/admin-api/guides/payment-methods/paypal.mdx +++ b/content/docs/admin-api/guides/payment-methods/paypal.mdx @@ -1,5 +1,7 @@ --- title: PayPal Admin API Guide +description: Create Admin API orders paid with PayPal using the redirect payment flow, with one-click upsells via Reference Transactions +capability_ids: [payments-gateways] sidebar_label: PayPal sidebar_position: 3 tags: diff --git a/content/docs/admin-api/guides/payment-methods/sepa-debit.mdx b/content/docs/admin-api/guides/payment-methods/sepa-debit.mdx index 799c2da3..b69f3b6c 100644 --- a/content/docs/admin-api/guides/payment-methods/sepa-debit.mdx +++ b/content/docs/admin-api/guides/payment-methods/sepa-debit.mdx @@ -1,5 +1,7 @@ --- title: SEPA Direct Debit Admin API Guide +description: Create Admin API orders paid with SEPA Direct Debit using the redirect payment flow, payment_return_url, and payment_complete_url +capability_ids: [payments-gateways] sidebar_label: SEPA Direct Debit sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/payment-methods/swish.mdx b/content/docs/admin-api/guides/payment-methods/swish.mdx index d591997e..659d8200 100644 --- a/content/docs/admin-api/guides/payment-methods/swish.mdx +++ b/content/docs/admin-api/guides/payment-methods/swish.mdx @@ -1,5 +1,7 @@ --- title: Swish Admin API Guide +description: Create Admin API orders paid with Swish through NEXT Payments for Swedish SEK checkouts using the redirect payment flow +capability_ids: [payments-gateways] sidebar_label: Swish tags: - Guide diff --git a/content/docs/admin-api/guides/payment-methods/twint.mdx b/content/docs/admin-api/guides/payment-methods/twint.mdx index 4a5c24c3..d6d424ae 100644 --- a/content/docs/admin-api/guides/payment-methods/twint.mdx +++ b/content/docs/admin-api/guides/payment-methods/twint.mdx @@ -1,5 +1,7 @@ --- title: Twint Admin API Guide +description: Create Admin API orders paid with Twint via NEXT Payments using the redirect payment flow, with support for one-click upsells and subscription items +capability_ids: [payments-gateways] sidebar_label: Twint sidebar_position: 5 tags: diff --git a/content/docs/admin-api/guides/subscription-management.mdx b/content/docs/admin-api/guides/subscription-management.mdx index 7fa8f3ae..c94bec2e 100644 --- a/content/docs/admin-api/guides/subscription-management.mdx +++ b/content/docs/admin-api/guides/subscription-management.mdx @@ -1,5 +1,7 @@ --- title: API Subscription Management +description: Create, update, pause, cancel, renew, and retry subscriptions through the Admin API, including line item, schedule, payment, and bulk operations +capability_ids: [subscriptions] sidebar_label: Subscription Management sidebar_position: 1 tags: diff --git a/content/docs/admin-api/guides/testing-guide.mdx b/content/docs/admin-api/guides/testing-guide.mdx index 192fc261..5169401e 100644 --- a/content/docs/admin-api/guides/testing-guide.mdx +++ b/content/docs/admin-api/guides/testing-guide.mdx @@ -1,5 +1,7 @@ --- title: Test Order Flows +description: Test cards, test card tokens, and the test gateway for creating test orders, transactions, and subscriptions on the Admin API +capability_ids: [testing] sidebar_label: Testing Guide sidebar_position: 1 tags: diff --git a/content/docs/admin-api/index.mdx b/content/docs/admin-api/index.mdx index 588a6cc9..f8490457 100644 --- a/content/docs/admin-api/index.mdx +++ b/content/docs/admin-api/index.mdx @@ -1,5 +1,7 @@ --- title: Admin API +description: Admin API authentication with OAuth apps and access tokens, API versioning with the X-29next-API-Version header, and rate limits +capability_ids: [admin-api, legacy-identifiers] sidebar_label: Admin API sidebar_position: 5 --- diff --git a/content/docs/admin-api/permissions.md b/content/docs/admin-api/permissions.md index 9e5a4d2e..9d786e8f 100644 --- a/content/docs/admin-api/permissions.md +++ b/content/docs/admin-api/permissions.md @@ -1,5 +1,7 @@ --- title: Permissions +description: OAuth app scopes that control read and write access to Admin API resources such as orders, carts, subscriptions, and webhooks +capability_ids: [admin-api] sidebar_label: Permissions sidebar_position: 2 --- diff --git a/content/docs/apps/app-development-flow.mdx b/content/docs/apps/app-development-flow.mdx index ffb02293..2c8d01e6 100644 --- a/content/docs/apps/app-development-flow.mdx +++ b/content/docs/apps/app-development-flow.mdx @@ -1,5 +1,7 @@ --- title: Development Flow +description: How app changes reach development stores automatically and production stores through versioned releases created in your Partner account +capability_ids: [apps] sidebar_position: 3 --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/app-kit.mdx b/content/docs/apps/app-kit.mdx index 5ea6dc64..92978193 100644 --- a/content/docs/apps/app-kit.mdx +++ b/content/docs/apps/app-kit.mdx @@ -1,5 +1,7 @@ --- title: App Kit +description: Install the next-app-kit Python package and use nak setup, nak build, and nak push to bundle and push app files to Next Commerce +capability_ids: [apps] sidebar_label: App Kit sidebar_position: 7 tags: diff --git a/content/docs/apps/assets.mdx b/content/docs/apps/assets.mdx index 3a12147b..660b833d 100644 --- a/content/docs/apps/assets.mdx +++ b/content/docs/apps/assets.mdx @@ -3,6 +3,7 @@ title: Assets Reference sidebar_label: Assets sidebar_position: 4 description: The asset directory is meant to contain any static assets needed for your app, such as images, css, or javascript that is referenced in your app snippets. +capability_ids: [apps] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/event-tracking.mdx b/content/docs/apps/event-tracking.mdx index 8c2c399b..6fa15e37 100644 --- a/content/docs/apps/event-tracking.mdx +++ b/content/docs/apps/event-tracking.mdx @@ -1,5 +1,7 @@ --- title: Event Tracking +description: Map a JavaScript file as storefront_event_tracker in manifest.json and read app settings from app.settings inside the tracker +capability_ids: [apps] sidebar_position: 5 --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/guides/dispute-service.mdx b/content/docs/apps/guides/dispute-service.mdx index bc13c84d..f1e9c627 100644 --- a/content/docs/apps/guides/dispute-service.mdx +++ b/content/docs/apps/guides/dispute-service.mdx @@ -1,5 +1,7 @@ --- title: Dispute Service Apps +description: Build a server-to-server app that creates, matches, refunds, and resolves payment disputes with the Admin API and transaction.created webhooks +capability_ids: [disputes] sidebar_position: 1 tags: - Guide diff --git a/content/docs/apps/guides/fulfillment-service.mdx b/content/docs/apps/guides/fulfillment-service.mdx index 1e03c812..f4dfbf56 100644 --- a/content/docs/apps/guides/fulfillment-service.mdx +++ b/content/docs/apps/guides/fulfillment-service.mdx @@ -1,5 +1,7 @@ --- title: Fulfillment Service Apps +description: Build a fulfillment service app that accepts fulfillment requests, creates full or partial fulfillments, handles cancellations, and syncs stock +capability_ids: [fulfillment] sidebar_position: 2 tags: - Guide diff --git a/content/docs/apps/guides/marketing-attribution.mdx b/content/docs/apps/guides/marketing-attribution.mdx index aff8b90b..19d94eac 100644 --- a/content/docs/apps/guides/marketing-attribution.mdx +++ b/content/docs/apps/guides/marketing-attribution.mdx @@ -1,5 +1,7 @@ --- title: Marketing Attribution Apps +description: Capture ad platform identifiers as cart attribution metadata with an event tracker and read them from order.created webhooks +capability_ids: [apps] sidebar_position: 3 tags: - Guide diff --git a/content/docs/apps/guides/server-to-server-apps.mdx b/content/docs/apps/guides/server-to-server-apps.mdx index 8b904d11..15883611 100644 --- a/content/docs/apps/guides/server-to-server-apps.mdx +++ b/content/docs/apps/guides/server-to-server-apps.mdx @@ -1,5 +1,7 @@ --- title: Server to Server Apps +description: Create a Partner account app, configure its OAuth URLs, and test the install flow on a development store with OAuth Debugger +capability_ids: [apps] sidebar_position: 0 tags: - Guide diff --git a/content/docs/apps/guides/storefront-extension.mdx b/content/docs/apps/guides/storefront-extension.mdx index 3ee66de6..e1aa3461 100644 --- a/content/docs/apps/guides/storefront-extension.mdx +++ b/content/docs/apps/guides/storefront-extension.mdx @@ -1,5 +1,7 @@ --- title: Storefront Extension Apps +description: App file layout, manifest.json, and using App Kit to set up, build, and push an app that extends storefront themes +capability_ids: [apps] sidebar_position: 0 tags: - Guide @@ -74,5 +76,5 @@ Now, push your app to your our platform using your username and password credent ```bash title="Push App" -nak pash +nak push ``` \ No newline at end of file diff --git a/content/docs/apps/index.mdx b/content/docs/apps/index.mdx index faeb036c..a0c4b11c 100644 --- a/content/docs/apps/index.mdx +++ b/content/docs/apps/index.mdx @@ -1,5 +1,7 @@ --- title: Apps +description: How apps extend Next Commerce with webhooks, the Admin API, event tracking, and snippets, with links to example apps and reference guides +capability_ids: [apps] sidebar_title: Apps --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/manifest.mdx b/content/docs/apps/manifest.mdx index 7ca635e9..db0e4b8e 100644 --- a/content/docs/apps/manifest.mdx +++ b/content/docs/apps/manifest.mdx @@ -1,5 +1,7 @@ --- title: Manifest Reference +description: "manifest.json properties for storefront apps: storefront_event_tracker, locations for app snippets, and settings_schema" +capability_ids: [apps] sidebar_label: Manifest sidebar_position: 3 --- diff --git a/content/docs/apps/oauth/getting-started.mdx b/content/docs/apps/oauth/getting-started.mdx index 6e37a036..6fddb198 100644 --- a/content/docs/apps/oauth/getting-started.mdx +++ b/content/docs/apps/oauth/getting-started.mdx @@ -1,5 +1,7 @@ --- title: Getting Started with OAuth +description: Authorize a server-side app with the OAuth 2.0 authorization code flow and exchange the code for an Admin API access token +capability_ids: [apps] sidebar_position: 1 --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/oauth/index.mdx b/content/docs/apps/oauth/index.mdx index 6bddd6dd..44c8caf0 100644 --- a/content/docs/apps/oauth/index.mdx +++ b/content/docs/apps/oauth/index.mdx @@ -1,5 +1,7 @@ --- title: OAuth Overview +description: How the OAuth 2.0 authorization code flow issues Admin API access tokens for server-side apps, with the install flow step by step +capability_ids: [apps] sidebar_position: 2 --- diff --git a/content/docs/apps/oauth/install-flows.md b/content/docs/apps/oauth/install-flows.md index b85b8466..0f45bc85 100644 --- a/content/docs/apps/oauth/install-flows.md +++ b/content/docs/apps/oauth/install-flows.md @@ -1,5 +1,7 @@ --- title: Install Flows +description: Build install links for private apps with client_id and what changes when an app is published as a public app +capability_ids: [apps] sidebar_position: 6 --- diff --git a/content/docs/apps/oauth/session-auth.mdx b/content/docs/apps/oauth/session-auth.mdx index dff33e38..cd1ec097 100644 --- a/content/docs/apps/oauth/session-auth.mdx +++ b/content/docs/apps/oauth/session-auth.mdx @@ -1,5 +1,7 @@ --- title: Session Token Overview +description: Verify short-lived JWT session tokens sent from the store dashboard using your app client ID and client secret +capability_ids: [apps] sidebar_position: 2 --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/apps/review.mdx b/content/docs/apps/review.mdx index 8a7156e7..0cc84062 100644 --- a/content/docs/apps/review.mdx +++ b/content/docs/apps/review.mdx @@ -1,5 +1,7 @@ --- -title: Submiting an App for Review +title: Submitting an App for Review +description: Checklist for submitting an app for review and how the review process decides whether it is published to the App store +capability_ids: [apps] sidebar_label: Review & Publishing sidebar_position: 8 --- diff --git a/content/docs/apps/settings.mdx b/content/docs/apps/settings.mdx index 24bf2064..5e8a1411 100644 --- a/content/docs/apps/settings.mdx +++ b/content/docs/apps/settings.mdx @@ -1,5 +1,7 @@ --- title: Settings Reference +description: Define app settings in settings_schema, read them from app.settings in snippets, and reference for setting attributes and input types +capability_ids: [apps] sidebar_label: Settings sidebar_position: 6 --- diff --git a/content/docs/apps/snippets.mdx b/content/docs/apps/snippets.mdx index f1ca0228..4a55c04c 100644 --- a/content/docs/apps/snippets.mdx +++ b/content/docs/apps/snippets.mdx @@ -1,5 +1,7 @@ --- title: Snippets +description: HTML template snippets that extend storefront themes through app_hook locations, using the same syntax as theme templates +capability_ids: [apps] sidebar_label: Snippets sidebar_position: 5 --- diff --git a/content/docs/campaigns/admin-api/index.mdx b/content/docs/campaigns/admin-api/index.mdx index 8fef8104..4cdac398 100644 --- a/content/docs/campaigns/admin-api/index.mdx +++ b/content/docs/campaigns/admin-api/index.mdx @@ -1,6 +1,7 @@ --- title: Campaigns Admin API description: Set up and manage campaigns programmatically instead of clicking through the dashboard. +capability_ids: [campaigns] tags: - Guide --- diff --git a/content/docs/campaigns/api/index.mdx b/content/docs/campaigns/api/index.mdx index 3bf1776c..015600cf 100644 --- a/content/docs/campaigns/api/index.mdx +++ b/content/docs/campaigns/api/index.mdx @@ -1,5 +1,7 @@ --- title: Campaign Cart API +description: "Campaign Cart API for external checkout funnels: session tracking, calculate cart, create cart, create order, upsells, and order retrieval" +capability_ids: [campaigns] sidebar_label: Campaign Cart API sidebar_position: 1 --- diff --git a/content/docs/campaigns/index.mdx b/content/docs/campaigns/index.mdx index 71371e9b..bf1f5665 100644 --- a/content/docs/campaigns/index.mdx +++ b/content/docs/campaigns/index.mdx @@ -1,5 +1,7 @@ --- title: Getting Started +description: Scaffold a campaign funnel with Campaign Page Kit, place a test order, and understand campaigns, packages, offers, domains, analytics, and hosting +capability_ids: [campaigns] --- import { Callout } from 'fumadocs-ui/components/callout'; import { CampaignFunnelFlow, CampaignAnatomy } from '@/components/campaign-concepts-flow'; diff --git a/content/docs/campaigns/page-kit.mdx b/content/docs/campaigns/page-kit.mdx index 39a52fb9..af0ee8a8 100644 --- a/content/docs/campaigns/page-kit.mdx +++ b/content/docs/campaigns/page-kit.mdx @@ -1,6 +1,7 @@ --- title: Page Kit description: Build, preview, and deploy multiple campaign funnels from a single repo. +capability_ids: [campaigns] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/campaigns/templates.mdx b/content/docs/campaigns/templates.mdx index e03cf5dd..9fb0eb07 100644 --- a/content/docs/campaigns/templates.mdx +++ b/content/docs/campaigns/templates.mdx @@ -1,6 +1,7 @@ --- title: Templates description: The starter templates you can install with page kit, get your campaign up and running in minutes. +capability_ids: [campaigns] --- ## Introduction diff --git a/content/docs/index.mdx b/content/docs/index.mdx index ea7c5343..af651be2 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,5 +1,6 @@ --- title: Getting Started +description: Overview of the Next Commerce developer platform with starting points for campaigns, storefront themes, the Admin API, and apps sidebar_position: 0 --- diff --git a/content/docs/skills/index.mdx b/content/docs/skills/index.mdx index 82032155..2c9af782 100644 --- a/content/docs/skills/index.mdx +++ b/content/docs/skills/index.mdx @@ -1,6 +1,7 @@ --- title: AI Skills description: Pre-built skills that give AI coding agents deep knowledge of the Next Commerce platform. +capability_ids: [agent-skills] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/checkout-links.mdx b/content/docs/storefront/checkout-links.mdx index 32e05f00..930899c4 100644 --- a/content/docs/storefront/checkout-links.mdx +++ b/content/docs/storefront/checkout-links.mdx @@ -1,5 +1,7 @@ --- title: Checkout Links +description: URL parameters for pre-loading a store's checkout with products, vouchers, currency, and marketing attribution +capability_ids: [checkout-links] --- Checkout Links allow you add links from any website, email or web marketing channel directly to your store's checkout flow with items pre-loaded in their cart. diff --git a/content/docs/storefront/event-tracking.mdx b/content/docs/storefront/event-tracking.mdx index f0262b0a..7921d15b 100644 --- a/content/docs/storefront/event-tracking.mdx +++ b/content/docs/storefront/event-tracking.mdx @@ -1,5 +1,7 @@ --- title: Event Tracking +description: Subscribe to storefront customer events with JavaScript event trackers, the init context helper, and the available event payloads +capability_ids: [storefront-themes] sidebar_label: Event Tracking sidebar_position: 3 --- diff --git a/content/docs/storefront/graphql/index.mdx b/content/docs/storefront/graphql/index.mdx index 099f3a3c..4aefdeb8 100644 --- a/content/docs/storefront/graphql/index.mdx +++ b/content/docs/storefront/graphql/index.mdx @@ -1,5 +1,7 @@ --- title: Storefront GraphQL API +description: Storefront GraphQL API endpoint, session authentication, GraphiQL explorer, and the cart, product, voucher, and account operations +capability_ids: [storefront-themes] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/index.md b/content/docs/storefront/index.md index 2be438f7..4cf5cd60 100644 --- a/content/docs/storefront/index.md +++ b/content/docs/storefront/index.md @@ -1,5 +1,7 @@ --- title: Storefront +description: "Overview of the storefront developer tools: themes, event tracking, and the Storefront GraphQL API" +capability_ids: [storefront-themes] --- The Next Commerce storefront is a flexible, customizable front-end layer for your ecommerce business. Whether you're building a completely custom storefront or enhancing an existing theme, this section will guide you through the tools and features available to developers. diff --git a/content/docs/storefront/themes/cdn-and-caching.mdx b/content/docs/storefront/themes/cdn-and-caching.mdx index 4f2d5010..9032b631 100644 --- a/content/docs/storefront/themes/cdn-and-caching.mdx +++ b/content/docs/storefront/themes/cdn-and-caching.mdx @@ -2,6 +2,8 @@ sidebar_label: CDN & Caching sidebar_position: 2 title: Storefront CDN & Caching +description: How storefront asset CDN, full page caching, and template caching work, and why to verify theme changes on the network domain +capability_ids: [storefront-themes] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/themes/guides/custom-page-templates.mdx b/content/docs/storefront/themes/guides/custom-page-templates.mdx index a16d6d41..812c675d 100644 --- a/content/docs/storefront/themes/guides/custom-page-templates.mdx +++ b/content/docs/storefront/themes/guides/custom-page-templates.mdx @@ -1,5 +1,7 @@ --- title: Custom Page Templates +description: Create page..html templates in templates/pages that extend the default page template and select them in the dashboard +capability_ids: [storefront-themes] sidebar_label: Custom Page Templates tags: - Guide diff --git a/content/docs/storefront/themes/guides/custom-product-templates.mdx b/content/docs/storefront/themes/guides/custom-product-templates.mdx index fe21d498..977b6615 100644 --- a/content/docs/storefront/themes/guides/custom-product-templates.mdx +++ b/content/docs/storefront/themes/guides/custom-product-templates.mdx @@ -1,5 +1,7 @@ --- title: Custom Product Templates +description: Create product..html templates in templates/catalogue that extend the default product template and select them per product +capability_ids: [storefront-themes] sidebar_label: Custom Product Templates tags: - Guide diff --git a/content/docs/storefront/themes/guides/personalized-products.mdx b/content/docs/storefront/themes/guides/personalized-products.mdx index e6ec9898..cb0aaaff 100644 --- a/content/docs/storefront/themes/guides/personalized-products.mdx +++ b/content/docs/storefront/themes/guides/personalized-products.mdx @@ -1,5 +1,7 @@ --- title: Personalized Products Guide +description: "Capture customer input as line item properties with properties[] inputs, show them in the cart, and pass them via the Storefront GraphQL API" +capability_ids: [storefront-themes] sidebar_label: Personalized Products tags: - Guide diff --git a/content/docs/storefront/themes/guides/product-metadata.md b/content/docs/storefront/themes/guides/product-metadata.md index 907cf255..65129163 100644 --- a/content/docs/storefront/themes/guides/product-metadata.md +++ b/content/docs/storefront/themes/guides/product-metadata.md @@ -1,5 +1,7 @@ --- title: Product Metadata +description: Render custom product metadata fields in theme templates with product.metadata. +capability_ids: [storefront-themes] sidebar_label: Product Metadata tags: - Guide diff --git a/content/docs/storefront/themes/guides/product-variants.mdx b/content/docs/storefront/themes/guides/product-variants.mdx index 620c7624..4eea7206 100644 --- a/content/docs/storefront/themes/guides/product-variants.mdx +++ b/content/docs/storefront/themes/guides/product-variants.mdx @@ -1,5 +1,7 @@ --- title: Product Variants Guide +description: Render variant attribute selectors from variant_form and map choices to variant product IDs with the product.data JSON object +capability_ids: [storefront-themes] sidebar_label: Product Variants tags: - Guide diff --git a/content/docs/storefront/themes/index.mdx b/content/docs/storefront/themes/index.mdx index df40d4f6..c88d4916 100644 --- a/content/docs/storefront/themes/index.mdx +++ b/content/docs/storefront/themes/index.mdx @@ -1,5 +1,7 @@ --- title: Themes +description: Theme directory structure for assets, configs, locales, layouts, partials, templates, and sass, plus an introduction to Theme Kit +capability_ids: [storefront-themes] sidebar_title: Themes sidebar_position: 0 --- diff --git a/content/docs/storefront/themes/settings.mdx b/content/docs/storefront/themes/settings.mdx index 00fc7f28..68c08577 100644 --- a/content/docs/storefront/themes/settings.mdx +++ b/content/docs/storefront/themes/settings.mdx @@ -1,5 +1,7 @@ --- title: Theme Settings +description: Define dashboard theme settings in settings_schema.json, read them in templates, and reference every schema input type +capability_ids: [storefront-themes] sidebar_label: Settings sidebar_position: 3 --- diff --git a/content/docs/storefront/themes/templates/filters.md b/content/docs/storefront/themes/templates/filters.md index ae58d260..8170dd0a 100644 --- a/content/docs/storefront/themes/templates/filters.md +++ b/content/docs/storefront/themes/templates/filters.md @@ -1,5 +1,7 @@ --- title: 'Filter Reference' +description: Built-in Django Template Language filters for lists, formatting, strings, currency, asset URLs, and math +capability_ids: [storefront-themes] --- ## Arrays & Lists diff --git a/content/docs/storefront/themes/templates/index.mdx b/content/docs/storefront/themes/templates/index.mdx index 6859976e..98dc1c23 100644 --- a/content/docs/storefront/themes/templates/index.mdx +++ b/content/docs/storefront/themes/templates/index.mdx @@ -1,5 +1,7 @@ --- title: Templates +description: "Introduction to the Django Template Language used in storefront themes: variables, filters, and tags" +capability_ids: [storefront-themes] sidebar_position: 1 --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/themes/templates/objects.mdx b/content/docs/storefront/themes/templates/objects.mdx index 388d8b78..5f1762ca 100644 --- a/content/docs/storefront/themes/templates/objects.mdx +++ b/content/docs/storefront/themes/templates/objects.mdx @@ -1,5 +1,7 @@ --- title: Object Reference +description: Global, page, and view-specific template objects available in storefront theme templates and their properties +capability_ids: [storefront-themes] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/themes/templates/tags.mdx b/content/docs/storefront/themes/templates/tags.mdx index 982575e7..20d9a729 100644 --- a/content/docs/storefront/themes/templates/tags.mdx +++ b/content/docs/storefront/themes/templates/tags.mdx @@ -1,5 +1,7 @@ --- title: Tag Reference +description: Built-in template tags for storefront themes, including extends, include, cart_form, app_hook, purchase_info_for_product, t, and url +capability_ids: [storefront-themes] sidebar_label: Tag Reference sidebar_position: 1 --- diff --git a/content/docs/storefront/themes/templates/urls-and-template-paths.mdx b/content/docs/storefront/themes/templates/urls-and-template-paths.mdx index f7f1fde3..e495688a 100644 --- a/content/docs/storefront/themes/templates/urls-and-template-paths.mdx +++ b/content/docs/storefront/themes/templates/urls-and-template-paths.mdx @@ -1,6 +1,8 @@ --- sidebar_label: URLs & Template Paths title: URLs & Template Paths +description: Storefront URL names, URL paths, and the theme template path each built-in view renders +capability_ids: [storefront-themes] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/storefront/themes/theme-kit.mdx b/content/docs/storefront/themes/theme-kit.mdx index a0a7e9e4..2a0671b5 100644 --- a/content/docs/storefront/themes/theme-kit.mdx +++ b/content/docs/storefront/themes/theme-kit.mdx @@ -1,5 +1,7 @@ --- title: Theme Kit +description: Install and configure ntk (Theme Kit), connect it to a store with an API key, and use its checkout, push, pull, watch, and sass commands +capability_ids: [storefront-themes] sidebar_label: Theme Kit sidebar_position: 4 tags: diff --git a/content/docs/storefront/themes/translations.mdx b/content/docs/storefront/themes/translations.mdx index 9e2feac9..a62b2eef 100644 --- a/content/docs/storefront/themes/translations.mdx +++ b/content/docs/storefront/themes/translations.mdx @@ -2,6 +2,8 @@ sidebar_label: Translations sidebar_position: 2 title: Translations +description: Localize theme templates with the t tag, locale JSON files, variable arguments, and cardinal and ordinal pluralization +capability_ids: [storefront-themes] --- Theme templates can be fully localized with translations so that your store visitors are shown content in their local language. Use the t (translation) tag in your templates to access string translations in the locale files. Learn more about the [t tag](/docs/storefront/themes/templates/tags#t) and theme Locale files. diff --git a/content/docs/testing/index.mdx b/content/docs/testing/index.mdx index 68ce36fc..f8dd56e0 100644 --- a/content/docs/testing/index.mdx +++ b/content/docs/testing/index.mdx @@ -1,6 +1,7 @@ --- title: Testing description: Test checkout, orders, subscriptions, and webhooks on a live store without moving real money +capability_ids: [testing] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/content/docs/webhooks/index.mdx b/content/docs/webhooks/index.mdx index dc301426..b7a4cae8 100644 --- a/content/docs/webhooks/index.mdx +++ b/content/docs/webhooks/index.mdx @@ -1,6 +1,7 @@ --- title: Webhooks description: Use webhooks to be notified about events that happen in your store. +capability_ids: [webhooks, legacy-identifiers] --- import { Callout } from 'fumadocs-ui/components/callout'; diff --git a/scripts/check-frontmatter.mjs b/scripts/check-frontmatter.mjs new file mode 100644 index 00000000..eeb1e390 --- /dev/null +++ b/scripts/check-frontmatter.mjs @@ -0,0 +1,140 @@ +/** + * Frontmatter checks for authored developer pages. + * + * Counting rule (frozen 2026-09-03 for the docs agent-accessibility packet): + * an "authored page" is a git-tracked file matching content/docs/**\/*.{md,mdx}. + * Generated reference trees are git-ignored, so they never count. At 3d089df + * this rule counted 67 authored pages, of which 60 lacked a description. + * + * Checks: + * 1. every authored page has a non-empty `description` + * 2. `audience`, `status`, `last_verified` use the enums/format in source.config.ts + * 3. every `capability_ids` entry exists in the generated capability map + * 4. every page the map cites in developer_docs declares that id in capability_ids + * (run with --write to insert the missing ids; the map is the source, the page + * field is the derived copy that lets the page be filtered without the map) + * + * Run after generate-capability-map.mjs. + */ + +import { execFileSync } from 'child_process'; +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { load as loadYaml } from 'js-yaml'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, '..'); +const WRITE = process.argv.includes('--write'); + +const MAP_PATH = join(ROOT, 'lib', 'generated', 'capabilities.json'); +if (!existsSync(MAP_PATH)) { + console.error('check-frontmatter: lib/generated/capabilities.json is missing; run `npm run generate` first'); + process.exit(1); +} +const map = JSON.parse(readFileSync(MAP_PATH, 'utf8')); +const DEVELOPER_SITE = map.sources.developer_docs; +const knownIds = new Set(map.capabilities.map((c) => c.id)); + +// page url -> set of capability ids the map says it belongs to +const expectedIds = new Map(); +for (const c of map.capabilities) { + for (const url of c.developer_docs) { + const path = url.replace(DEVELOPER_SITE, ''); + const set = expectedIds.get(path) ?? new Set(); + set.add(c.id); + expectedIds.set(path, set); + } +} + +const AUDIENCES = new Set(['merchant', 'developer']); +const STATUSES = new Set(['available', 'beta', 'deprecated']); +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +const files = execFileSync('git', ['ls-files', '--', 'content/docs/**/*.md', 'content/docs/**/*.mdx', 'content/docs/*.md', 'content/docs/*.mdx'], { + cwd: ROOT, + encoding: 'utf8', +}) + .split('\n') + .filter(Boolean); + +function pageUrl(file) { + return ( + '/docs/' + + file + .replace(/^content\/docs\//, '') + .replace(/\.(md|mdx)$/, '') + .replace(/(^|\/)index$/, '') + ).replace(/\/$/, '') || '/docs'; +} + +function splitFrontmatter(text) { + const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!m) return null; + return { raw: m[1], end: m[0].length }; +} + +const errors = []; +let missingDescriptions = 0; +let rewritten = 0; + +for (const file of files) { + const abs = join(ROOT, file); + const text = readFileSync(abs, 'utf8'); + const fm = splitFrontmatter(text); + if (!fm) { + errors.push(`${file}: no frontmatter`); + continue; + } + let data; + try { + data = loadYaml(fm.raw) ?? {}; + } catch (e) { + errors.push(`${file}: frontmatter is not valid YAML (${e.message.split('\n')[0]})`); + continue; + } + + if (typeof data.description !== 'string' || data.description.trim() === '') { + errors.push(`${file}: missing description`); + missingDescriptions += 1; + } + if (data.audience !== undefined) { + if (!Array.isArray(data.audience) || data.audience.some((a) => !AUDIENCES.has(a))) + errors.push(`${file}: audience must be a list drawn from merchant, developer`); + } + if (data.status !== undefined && !STATUSES.has(data.status)) + errors.push(`${file}: status must be one of available, beta, deprecated`); + if (data.last_verified !== undefined && !DATE_RE.test(String(data.last_verified))) + errors.push(`${file}: last_verified must be YYYY-MM-DD`); + + const declared = Array.isArray(data.capability_ids) ? data.capability_ids.map(String) : []; + for (const id of declared) if (!knownIds.has(id)) errors.push(`${file}: unknown capability id ${id}`); + + const expected = expectedIds.get(pageUrl(file)) ?? new Set(); + const missing = [...expected].filter((id) => !declared.includes(id)); + if (missing.length > 0) { + if (WRITE) { + const all = [...new Set([...declared, ...missing])]; + const line = `capability_ids: [${all.join(', ')}]`; + const raw = data.capability_ids === undefined + ? fm.raw.replace(/^(description:[^\n]*)$/m, `$1\n${line}`) + : fm.raw.replace(/^capability_ids:[^\n]*(\n\s+-[^\n]*)*/m, line); + if (raw === fm.raw) { + errors.push(`${file}: could not insert capability_ids (no description line to anchor on)`); + } else { + writeFileSync(abs, `---\n${raw}\n---\n` + text.slice(fm.end)); + rewritten += 1; + } + } else { + errors.push(`${file}: capability map cites this page for ${missing.join(', ')} but capability_ids does not declare it (run check-frontmatter --write)`); + } + } +} + +console.log(`check-frontmatter: ${files.length} authored pages checked${WRITE ? `, ${rewritten} rewritten` : ''}`); +if (errors.length > 0) { + console.error(`check-frontmatter: FAIL (${errors.length} problems, ${missingDescriptions} missing descriptions)`); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} +console.log('check-frontmatter: OK'); From 55f1096e72e48dff7b19f717e1e82b9ae5b199d9 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Thu, 3 Sep 2026 16:29:38 +0700 Subject: [PATCH 4/7] Reciprocal merchant links on developer pages, live surface checks Every developer page the map cites (or that declares capability_ids) renders a panel linking the merchant guides for the same capability; the post-build check asserts the panel on every cited page. AGENTS.md and the 404 page point at the map and bundles. check-live-surfaces.mjs runs against the deployed sites weekly: sitemap, robots, llms.txt, map and bundle integrity, 404 recovery, the merchant search budget, and the deterministic half of the 10-question smoke set. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/live-surfaces.yml | 23 ++++ AGENTS.md | 6 +- app/docs/[[...slug]]/page.tsx | 2 + app/not-found.tsx | 1 + components/capability-links.tsx | 56 +++++++++ scripts/check-live-surfaces.mjs | 179 ++++++++++++++++++++++++++++ 6 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/live-surfaces.yml create mode 100644 components/capability-links.tsx create mode 100644 scripts/check-live-surfaces.mjs diff --git a/.github/workflows/live-surfaces.yml b/.github/workflows/live-surfaces.yml new file mode 100644 index 00000000..96dbbb93 --- /dev/null +++ b/.github/workflows/live-surfaces.yml @@ -0,0 +1,23 @@ +name: Live agent surfaces + +# Checks the deployed sites, not the branch: sitemap, robots, llms.txt links, +# capability map, bundles, 404 recovery, search index budget, and the +# deterministic half of the prospect-agent smoke set. Runs on a schedule and +# by hand; a failure here means production drifted, not that a PR is wrong. + +on: + schedule: + - cron: '17 3 * * 1' # Mondays 03:17 UTC + workflow_dispatch: + +jobs: + live: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: '22' + + - run: node scripts/check-live-surfaces.mjs diff --git a/AGENTS.md b/AGENTS.md index e3abe573..c5c5dd1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,8 +10,10 @@ The sibling site, https://docs.nextcommerce.com, is the merchant and operator do ## Where to start -- https://developers.nextcommerce.com/llms.txt is the page index for this site, with a one-line description per page and absolute URLs. -- https://developers.nextcommerce.com/llms-full.txt is the full corpus in one file. It is large (about 1.5 MB); fetch it only when you need broad coverage rather than a specific page. +- https://developers.nextcommerce.com/llms.txt is the page index for this site, with a one-line description per page and absolute URLs. It also lists the domain bundles. +- https://developers.nextcommerce.com/capabilities.json is the platform capability map: one record per capability with a stable id, the merchant and developer pages that document it, its Admin API operations, webhook events, skills, status, and the date its links were last verified. The readable form is https://developers.nextcommerce.com/docs/capabilities. Pages on both sites declare their ids in a `capability_ids` frontmatter field. +- Domain bundles at https://developers.nextcommerce.com/llms/.txt (`platform`, `admin-api`, `payments`, `campaigns`, `storefront`, `apps-webhooks`) are plain Markdown: the capability records for one domain followed by the full text of the developer pages they cite. Fetch the bundle for your question before the full corpus. +- https://developers.nextcommerce.com/llms-full.txt is the full corpus in one file. It is large (about 1.5 MB) and includes 500+ generated reference pages; fetch it only when a bundle or a page URL is not enough. - Raw OpenAPI specs, which are the authority for operations, parameters, and fields: - https://developers.nextcommerce.com/api/admin/2024-04-01.yaml (stable) - https://developers.nextcommerce.com/api/admin/unstable.yaml diff --git a/app/docs/[[...slug]]/page.tsx b/app/docs/[[...slug]]/page.tsx index 7b1b3587..00098a89 100644 --- a/app/docs/[[...slug]]/page.tsx +++ b/app/docs/[[...slug]]/page.tsx @@ -11,6 +11,7 @@ import { getMDXComponents } from '@/components/mdx'; import { createRelativeLink } from 'fumadocs-ui/mdx'; import { VersionSelector } from '@/components/version-selector'; import { AutoExpandBody } from '@/components/auto-expand-body'; +import { CapabilityLinks } from '@/components/capability-links'; import type { Metadata } from 'next'; export default async function Page(props: { @@ -41,6 +42,7 @@ export default async function Page(props: { a: createRelativeLink(source, page), })} /> + ); diff --git a/app/not-found.tsx b/app/not-found.tsx index 6925faeb..e9e0495d 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -15,6 +15,7 @@ export default function NotFound() {
  • Browse all docs
  • Admin API
  • Webhooks
  • +
  • Capability map
  • Merchant docs
  • Changelog
  • Agent index (llms.txt)
  • diff --git a/components/capability-links.tsx b/components/capability-links.tsx new file mode 100644 index 00000000..32a594a9 --- /dev/null +++ b/components/capability-links.tsx @@ -0,0 +1,56 @@ +import { capabilitiesForPage, getCapability, MERCHANT_SITE, type Capability } from '@/lib/capabilities'; + +/** + * Reciprocal links from a developer page back to the merchant guides for the + * same capability. Driven entirely by the capability map: a page is linked when + * the map's developer_docs cite it or its frontmatter declares capability_ids. + */ +export function CapabilityLinks({ pageUrl, declaredIds }: { pageUrl: string; declaredIds?: string[] }) { + const byId = new Map(); + for (const c of capabilitiesForPage(pageUrl)) byId.set(c.id, c); + for (const id of declaredIds ?? []) { + const c = getCapability(id); + if (c) byId.set(c.id, c); + } + const capabilities = [...byId.values()]; + if (capabilities.length === 0) return null; + + const merchantLinks = capabilities.flatMap((c) => + c.operator_docs.map((url) => ({ url, capability: c })), + ); + + return ( + + ); +} diff --git a/scripts/check-live-surfaces.mjs b/scripts/check-live-surfaces.mjs new file mode 100644 index 00000000..e947ba97 --- /dev/null +++ b/scripts/check-live-surfaces.mjs @@ -0,0 +1,179 @@ +/** + * Live checks against the two deployed documentation sites. Network only; no + * build needed. Exit 1 on any failure. Run by .github/workflows/live-surfaces.yml + * on a schedule and by `npm run check-live-surfaces`. + * + * Two groups: + * 1. Entry surfaces: sitemap, robots, llms.txt (absolute and reciprocal links), + * capability map and bundles, semantic text output, 404 recovery links, + * merchant search index budget. + * 2. The deterministic half of the 10-question prospect-agent smoke set + * (executive packet docs-agent-accessibility-review, step 1). Each check is + * a fact a page must state, not a judgement about an answer; the judged + * half still needs a cold agent run. + * + * Override hosts with DEVELOPER_SITE / MERCHANT_SITE to point at a preview. + */ + +const DEV = (process.env.DEVELOPER_SITE ?? 'https://developers.nextcommerce.com').replace(/\/$/, ''); +const MERCHANT = (process.env.MERCHANT_SITE ?? 'https://docs.nextcommerce.com').replace(/\/$/, ''); +const SEARCH_INDEX_BUDGET_BYTES = 6_000_000; // mirrors docs/scripts/check-search-budget.mjs +const BUNDLE_BUDGET_BYTES = 400_000; // mirrors scripts/check-agent-surfaces.mjs + +const failures = []; +const passes = []; +function check(name, condition, detail = '') { + if (condition) passes.push(name); + else failures.push(`${name}${detail ? `: ${detail}` : ''}`); +} + +const cache = new Map(); +async function fetchText(url) { + if (cache.has(url)) return cache.get(url); + const res = await fetch(url, { redirect: 'manual', headers: { 'user-agent': 'next-docs-live-check/1' } }); + const body = await res.text(); + const out = { status: res.status, headers: res.headers, body, bytes: Buffer.byteLength(body, 'utf8') }; + cache.set(url, out); + return out; +} + +async function page(url) { + const r = await fetchText(url); + check(`200 ${url}`, r.status === 200, `status ${r.status}`); + return r; +} + +// ---- 1. entry surfaces ------------------------------------------------------- + +for (const site of [DEV, MERCHANT]) { + const sitemap = await page(`${site}/sitemap.xml`); + check(`${site} sitemap is XML`, sitemap.body.trimStart().startsWith('= 8); + const ids = new Set(capabilityMap.capabilities.map((c) => c.id)); + for (const id of ['testing', 'subscriptions', 'payments-gateways', 'webhooks', 'campaigns', 'storefront-themes', 'admin-api', 'legacy-identifiers']) { + check(`capabilities.json has ${id}`, ids.has(id)); + } + const capPage = await page(`${DEV}/docs/capabilities`); + for (const c of capabilityMap.capabilities) check(`capability page anchors ${c.id}`, capPage.body.includes(`id="${c.id}"`)); + + // Every link the map makes must resolve on the live sites. + const linked = new Set(); + for (const c of capabilityMap.capabilities) { + for (const u of [...c.operator_docs, ...c.developer_docs]) linked.add(u); + for (const op of c.api_operations) if (op.url) linked.add(op.url); + for (const w of c.webhooks) if (w.url) linked.add(w.url); + } + let broken = 0; + await Promise.all( + [...linked].map(async (u) => { + const r = await fetchText(u); + if (r.status !== 200) { + broken += 1; + failures.push(`map link ${u} returned ${r.status}`); + } + }), + ); + check(`all ${linked.size} capability-map links resolve`, broken === 0); + + for (const b of capabilityMap.bundles) { + const r = await page(b.url); + check(`bundle ${b.id} is plain text`, (r.headers.get('content-type') ?? '').startsWith('text/plain')); + check(`bundle ${b.id} within ${BUNDLE_BUDGET_BYTES} bytes`, r.bytes <= BUNDLE_BUDGET_BYTES, `${r.bytes} bytes`); + const prose = r.body.replace(/^(```|~~~)[\s\S]*?^\1[^\n]*$/gm, ''); + check(`bundle ${b.id} has no MDX residue`, !/^(import|export)\s|<\/?[A-Z][A-Za-z0-9]*[\s>/]/m.test(prose)); + check(`bundle ${b.id} is not framework serialization`, !r.body.includes('self.__next_f') && !r.body.startsWith('0:')); + check(`bundle ${b.id} has a Pages section`, r.body.includes('## Pages')); + } +} + +const full = await page(`${DEV}/llms-full.txt`); +check('llms-full.txt is text', (full.headers.get('content-type') ?? '').startsWith('text/plain')); + +const search = await fetchText(`${MERCHANT}/api/search`); +check('merchant search index responds', search.status === 200, `status ${search.status}`); +check(`merchant search index within ${SEARCH_INDEX_BUDGET_BYTES} bytes`, search.bytes <= SEARCH_INDEX_BUDGET_BYTES, `${search.bytes} bytes`); +check('merchant search index is cached for an hour', /max-age=3600/.test(search.headers.get('cache-control') ?? ''), search.headers.get('cache-control') ?? 'no cache-control'); +check('merchant search index mentions subscriptions', /subscription/i.test(search.body)); + +// ---- 2. deterministic half of the smoke set -------------------------------- + +const testing = await page(`${DEV}/docs/testing`); +check('Q2 testing page says there is no separate sandbox', /no separate sandbox/i.test(testing.body)); +check('Q2 testing page lists the test card', testing.body.includes('6011111111111117')); +const testOrders = await page(`${MERCHANT}/docs/manage/orders/test-orders`); +check('Q2 merchant test-orders page exists and names test cards', /test (order )?card/i.test(testOrders.body)); + +const themeKit = await page(`${DEV}/docs/storefront/themes/theme-kit`); +check('Q3 theme kit page names ntk', /\bntk\b/.test(themeKit.body)); +const themesOverview = await page(`${DEV}/docs/storefront/themes`); +check('Q3 themes overview links its own Theme Kit page', themesOverview.body.includes('/docs/storefront/themes/theme-kit')); + +const webhooks = await page(`${DEV}/docs/webhooks`); +check('Q4 webhooks page explains renewals via transaction.created', webhooks.body.includes('transaction.created') && webhooks.body.includes('billing_cycle')); +check('Q4 webhooks page does not document a subscription.renewed event', !webhooks.body.includes('subscription.renewed')); +const subGuide = await page(`${DEV}/docs/admin-api/guides/subscription-management`); +check('Q4 subscription guide exists', subGuide.status === 200); + +const bankcard = await page(`${DEV}/docs/admin-api/guides/payment-methods/bankcard`); +check('Q5 bankcard guide documents payment_gateway selection', bankcard.body.includes('payment_gateway')); +const externalCheckout = await page(`${DEV}/docs/admin-api/guides/external-checkout`); +check('Q5 external checkout guide cross-references gateway selection', externalCheckout.body.includes('payment_gateway')); +await page(`${MERCHANT}/docs/features/payments`); + +const adminApi = await page(`${DEV}/docs/admin-api`); +check('Q6 Admin API page explains the 29next legacy name', /formerly 29 ?Next/i.test(adminApi.body)); +check('Q6 developer llms.txt carries the legacy identifiers note', /Legacy identifiers/.test((await fetchText(`${DEV}/llms.txt`)).body)); +check('Q6 merchant llms.txt carries the legacy identifiers note', /29 ?Next/.test((await fetchText(`${MERCHANT}/llms.txt`)).body)); + +const nextPayments = await page(`${MERCHANT}/docs/features/payments/gateways/next-payments`); +check('Q7 NEXT Payments page publishes no processing rate', !/\d+(\.\d+)?\s?%\s*(\+|plus)?\s*\$?\d*\.?\d*\s*(per|\/)\s*transaction/i.test(nextPayments.body)); + +check('Q8 Admin API page names 2024-04-01 as stable', adminApi.body.includes('2024-04-01') && /stable/i.test(adminApi.body)); +await page(`${MERCHANT}/changelog`); +const oldRef = await fetchText(`${DEV}/docs/admin-api/reference/?v=2024-04-01`); +check('Q8 changelog\'s old reference URL redirects or resolves', [200, 301, 302, 308].includes(oldRef.status), `status ${oldRef.status}`); + +if (capabilityMap) { + const webhookCap = capabilityMap.capabilities.find((c) => c.id === 'webhooks'); + const count = webhookCap?.webhooks.length ?? 0; + check('Q9 webhook count is 24 in the map', count === 24, `${count}`); + check('Q9 home page states the same webhook count as the map', (await page(DEV)).body.includes(`${count} Webhook Events`)); + check('Q9 dispute events exist', webhookCap?.webhooks.some((w) => w.event === 'dispute.created')); +} + +const campaigns = await page(`${DEV}/docs/campaigns`); +check('Q10 campaigns page exists', campaigns.status === 200); +await page(`${MERCHANT}/docs/apps/campaigns-app`); +await page(`${DEV}/docs/storefront/checkout-links`); + +// ---- report ----------------------------------------------------------------- + +console.log(`check-live-surfaces: ${passes.length} passed, ${failures.length} failed`); +for (const f of failures) console.error(` - ${f}`); +process.exit(failures.length > 0 ? 1 : 0); From 339676e8a3eaf1da49a961f35e1d2d17d5019ff3 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Thu, 3 Sep 2026 16:51:44 +0700 Subject: [PATCH 5/7] Review fixes: dedupe merchant links, escape bundle text in llms.txt, fail on unmatched operation tags, least-privilege workflows Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 7 +++++++ .github/workflows/live-surfaces.yml | 7 +++++++ app/llms.txt/route.ts | 2 +- components/capability-links.tsx | 5 ++--- scripts/generate-capability-map.mjs | 17 ++++++++++++++--- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba0e46e4..92461a88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,13 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: build-and-validate: runs-on: ubuntu-latest diff --git a/.github/workflows/live-surfaces.yml b/.github/workflows/live-surfaces.yml index 96dbbb93..ab62389f 100644 --- a/.github/workflows/live-surfaces.yml +++ b/.github/workflows/live-surfaces.yml @@ -10,6 +10,13 @@ on: - cron: '17 3 * * 1' # Mondays 03:17 UTC workflow_dispatch: +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: live: runs-on: ubuntu-latest diff --git a/app/llms.txt/route.ts b/app/llms.txt/route.ts index 4659197c..6e96e5cd 100644 --- a/app/llms.txt/route.ts +++ b/app/llms.txt/route.ts @@ -81,7 +81,7 @@ function header(): string { '', 'Fetch the bundle for your question before the full corpus. Each is plain Markdown: the capability records for one domain followed by the full text of the developer pages they cite.', '', - ...capabilityMap.bundles.map((b) => `- [${b.title}](${b.url}): ${b.intro}`), + ...capabilityMap.bundles.map((b) => `- [${linkText(b.title)}](${b.url}): ${oneLine(b.intro)}`), `- [Full corpus](${SITE}/llms-full.txt): every page in one file, including 500+ generated reference pages (large, about 1.5 MB); use a bundle or a page URL instead unless you need everything`, '', '## Legacy identifiers', diff --git a/components/capability-links.tsx b/components/capability-links.tsx index 32a594a9..6c03492e 100644 --- a/components/capability-links.tsx +++ b/components/capability-links.tsx @@ -15,9 +15,8 @@ export function CapabilityLinks({ pageUrl, declaredIds }: { pageUrl: string; dec const capabilities = [...byId.values()]; if (capabilities.length === 0) return null; - const merchantLinks = capabilities.flatMap((c) => - c.operator_docs.map((url) => ({ url, capability: c })), - ); + // Several capabilities on one page can cite the same merchant guide; list it once. + const merchantLinks = [...new Set(capabilities.flatMap((c) => c.operator_docs))].map((url) => ({ url })); return (